diff --git a/generators/csharp/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap b/generators/csharp/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap index 11f2d100761e..b6884c3756a7 100644 --- a/generators/csharp/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap +++ b/generators/csharp/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap @@ -379,13 +379,16 @@ await client.Service.JustFileWithQueryParamsAsync( exports[`snippets (default) > imdb > 'GET /movies/{movieId} (simple)' 1`] = ` "using Acme; +using Acme.Imdb; var client = new AcmeClient( token: "" ); await client.Imdb.GetMovieAsync( - "movie_xyz" + new GetMovieImdbRequest { + MovieID = "movie_xyz" + } ); " `; diff --git a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap index a394d17cf77c..e3951b3b8360 100644 --- a/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap +++ b/generators/go-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap @@ -521,6 +521,7 @@ exports[`snippets (default) > imdb > 'GET /movies/{movieId} (simple)' 1`] = ` import ( context "context" + acme "github.com/acme/acme-go" client "github.com/acme/acme-go/client" option "github.com/acme/acme-go/option" ) @@ -531,9 +532,12 @@ func do() { "", ), ) + request := &acme.GetMovieImdbRequest{ + MovieID: "movie_xyz", + } client.Imdb.GetMovie( context.TODO(), - "movie_xyz", + request, ) } " @@ -1493,6 +1497,7 @@ exports[`snippets (exportAllRequestsAtRoot) > imdb > 'GET /movies/{movieId} (sim import ( context "context" + acme "github.com/acme/acme-go" client "github.com/acme/acme-go/client" option "github.com/acme/acme-go/option" ) @@ -1503,9 +1508,12 @@ func do() { "", ), ) + request := &acme.GetMovieImdbRequest{ + MovieID: "movie_xyz", + } client.Imdb.GetMovie( context.TODO(), - "movie_xyz", + request, ) } " @@ -2465,6 +2473,7 @@ exports[`snippets (exportedClientName) > imdb > 'GET /movies/{movieId} (simple)' import ( context "context" + acme "github.com/acme/acme-go" client "github.com/acme/acme-go/client" option "github.com/acme/acme-go/option" ) @@ -2475,9 +2484,12 @@ func do() { "", ), ) + request := &acme.GetMovieImdbRequest{ + MovieID: "movie_xyz", + } client.Imdb.GetMovie( context.TODO(), - "movie_xyz", + request, ) } " diff --git a/generators/java-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap b/generators/java-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap index df16f26eb343..d98c93faa9dc 100644 --- a/generators/java-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap +++ b/generators/java-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap @@ -420,13 +420,19 @@ exports[`snippets (default) > imdb > 'GET /movies/{movieId} (simple)' 1`] = ` "package com.example.usage; import com.acme.acme.AcmeAcmeClient; +import com.acme.acme.types.GetMovieImdbRequest; AcmeAcmeClient client = AcmeAcmeClient .builder() .token("") .build(); -client.imdb().getMovie("movie_xyz"); +client.imdb().getMovie( + "movie_xyz", + GetMovieImdbRequest + .builder() + .build() +); " `; diff --git a/generators/php/base/src/AsIs.ts b/generators/php/base/src/AsIs.ts index 0e744ec3ef9e..a65d21bb392a 100644 --- a/generators/php/base/src/AsIs.ts +++ b/generators/php/base/src/AsIs.ts @@ -13,7 +13,14 @@ export enum AsIsFiles { RetryDecoratingClient = "Client/RetryDecoratingClient.Template.php", HttpClientBuilder = "Client/HttpClientBuilder.Template.php", RawClientTest = "Client/RawClientTest.Template.php", + StreamTest = "Client/StreamTest.Template.php", MockHttpClient = "Client/MockHttpClient.Template.php", + Stream = "Client/Stream.Template.php", + StreamFormat = "Client/StreamFormat.Template.php", + SseStream = "Client/SseStream.Template.php", + SseEvent = "Client/SseEvent.Template.php", + JsonStream = "Client/JsonStream.Template.php", + TextStream = "Client/TextStream.Template.php", // Core/Json files. JsonApiRequest = "Json/JsonApiRequest.Template.php", diff --git a/generators/php/base/src/asIs/Client/JsonStream.Template.php b/generators/php/base/src/asIs/Client/JsonStream.Template.php new file mode 100644 index 000000000000..88b319bb96d7 --- /dev/null +++ b/generators/php/base/src/asIs/Client/JsonStream.Template.php @@ -0,0 +1,39 @@ +; + +use Closure; +use Psr\Http\Message\ResponseInterface; + +/** + * Iterates a newline-delimited JSON (NDJSON) response body, yielding one + * deserialized chunk per non-empty line. + * + * @template T + * @extends Stream + */ +class JsonStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per line with the raw + * JSON payload string. + * @param ?string $terminator Optional sentinel line that ends the stream + * when received. Pass `null` to read until EOF. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = null, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Json, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/generators/php/base/src/asIs/Client/SseEvent.Template.php b/generators/php/base/src/asIs/Client/SseEvent.Template.php new file mode 100644 index 000000000000..ee18d630c7b8 --- /dev/null +++ b/generators/php/base/src/asIs/Client/SseEvent.Template.php @@ -0,0 +1,32 @@ +; + +/** + * A single Server-Sent Event with its WHATWG metadata fields. + * + * Returned by `SseStream::events()`. Use plain `foreach ($stream as $payload)` if + * metadata isn't needed and you only want the deserialized `data` field. + * + * @template T + */ +final class SseEvent +{ + /** + * @param T $data Deserialized payload from the `data:` field(s). Multi-line + * data is joined with a single newline before deserialization. + * @param string $event Value of the `event:` field, or empty string if not set. + * @param string $id Most recent `id:` field value. Per the WHATWG spec, this + * persists across events: a subsequent event without an explicit `id:` + * inherits the previous one. Empty string until the first id is observed. + * @param ?int $retry Reconnection time in milliseconds from the `retry:` field, + * or null if not set or unparseable. + */ + public function __construct( + public readonly mixed $data, + public readonly string $event = '', + public readonly string $id = '', + public readonly ?int $retry = null, + ) { + } +} diff --git a/generators/php/base/src/asIs/Client/SseStream.Template.php b/generators/php/base/src/asIs/Client/SseStream.Template.php new file mode 100644 index 000000000000..11278e2c8659 --- /dev/null +++ b/generators/php/base/src/asIs/Client/SseStream.Template.php @@ -0,0 +1,101 @@ +; + +use Closure; +use Generator; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; + +/** + * Iterates a `text/event-stream` (SSE) response body, yielding one deserialized + * event per dispatched frame. + * + * @template T + * @extends Stream + */ +class SseStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per dispatched event + * with the raw `data:` payload string (newline-joined for multi-line frames). + * @param ?string $terminator Optional sentinel payload that ends the stream + * when received. Defaults to '[DONE]', a common SSE convention. + * Pass `null` to disable terminator handling. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = '[DONE]', + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + self::validateContentType($response); + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Sse, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } + + /** + * Iterates the stream yielding both the deserialized payload and the + * accompanying SSE metadata (event type, id, retry). Use this when you + * need the event field (e.g. for event-typed unions) or `Last-Event-ID` + * for resumption logic. + * + * For data-only iteration, use this object directly as an iterable: + * `foreach ($stream as $event) { ... }`. + * + * @return Generator> + */ + public function events(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield new SseEvent( + data: $this->deserialize($raw['data']), + event: $raw['event'], + id: $raw['id'], + retry: $raw['retry'], + ); + } + } + + /** + * Validates that the response's Content-Type matches an SSE stream. + * + * Per WHATWG, the SSE wire format is always UTF-8; we reject explicit + * non-UTF-8 charset parameters rather than risk silent mojibake. A missing + * Content-Type header is tolerated — some servers omit it on streaming + * responses — but a wrong media type or wrong charset always throws. + */ + private static function validateContentType(ResponseInterface $response): void + { + $contentType = $response->getHeaderLine('Content-Type'); + if ($contentType === '') { + return; + } + $parts = explode(';', $contentType); + $mediaType = strtolower(trim($parts[0])); + if ($mediaType !== 'text/event-stream') { + throw new RuntimeException( + "Expected Content-Type 'text/event-stream' for SSE response, got '{$mediaType}'", + ); + } + foreach (array_slice($parts, 1) as $param) { + $param = trim($param); + if (stripos($param, 'charset=') !== 0) { + continue; + } + $charset = strtolower(trim(substr($param, 8), " \"'")); + if ($charset !== '' && $charset !== 'utf-8' && $charset !== 'utf8') { + throw new RuntimeException( + "Unsupported SSE charset '{$charset}'; per the WHATWG spec only UTF-8 is permitted", + ); + } + } + } +} diff --git a/generators/php/base/src/asIs/Client/Stream.Template.php b/generators/php/base/src/asIs/Client/Stream.Template.php new file mode 100644 index 000000000000..1aea256847b6 --- /dev/null +++ b/generators/php/base/src/asIs/Client/Stream.Template.php @@ -0,0 +1,287 @@ +; + +use Closure; +use Generator; +use IteratorAggregate; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use RuntimeException; + +/** + * Iterates a streaming HTTP response body frame-by-frame. + * + * Parameterized by `$format`, which selects the framing strategy: + * - 'sse' — WHATWG Server-Sent Events: lines starting with `data:` (and + * optionally `event:`, `id:`, `retry:`); a blank line dispatches + * the accumulated event. Multi-line `data` fields are joined + * with newlines. + * - 'json' — newline-delimited JSON: each non-empty line is one frame. + * - 'text' — newline-delimited text: each line is yielded as a raw string. + * + * When `$terminator` is set, an event whose payload equals the terminator + * ends iteration cleanly (e.g. `[DONE]` for SSE). + * + * A `$maxBufferSize` cap (default 1 MiB) guards against malformed streams + * that never emit a frame boundary: if the line buffer or a single event's + * data exceeds the cap, iteration aborts with `RuntimeException`. + * + * Direct instantiation is not supported; use `SseStream`, `JsonStream`, or + * `TextStream`. + * + * @template T + * @implements IteratorAggregate + */ +class Stream implements IteratorAggregate +{ + public const DEFAULT_MAX_BUFFER_SIZE = 1_048_576; + + private const READ_CHUNK_SIZE = 8192; + private const UTF8_BOM = "\xEF\xBB\xBF"; + + private StreamInterface $body; + + /** @var Closure(string): T */ + private Closure $deserializer; + + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per frame with the raw payload string. + * For text streams, the deserializer is typically `fn(string $line) => $line`. + * @param StreamFormat $format Framing strategy for the stream. + * @param ?string $terminator Optional sentinel value that ends the stream when matched + * against the raw frame payload (e.g. '[DONE]'). + * @param int $maxBufferSize Maximum size in bytes for the line buffer or a single SSE + * event's accumulated `data` field. Exceeding this throws `RuntimeException` to + * guard against pathological streams. Defaults to 1 MiB. + */ + protected function __construct( + ResponseInterface $response, + Closure $deserializer, + private readonly StreamFormat $format = StreamFormat::Sse, + private readonly ?string $terminator = null, + private readonly int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + $this->body = $response->getBody(); + $this->deserializer = $deserializer; + } + + /** + * Iteration is one-shot: PSR-7 bodies are forward-only, so re-iterating the + * same `Stream` instance yields nothing useful. + * + * @return Generator + */ + public function getIterator(): Generator + { + return match ($this->format) { + StreamFormat::Sse => $this->iterateSse(), + StreamFormat::Json => $this->iterateDelimited(), + StreamFormat::Text => $this->iterateText(), + }; + } + + /** + * Applies the configured deserializer to a single raw frame payload. + * Available to subclasses (notably `SseStream::events()`) so they can + * construct typed envelopes without accessing the closure directly. + * + * @return T + */ + protected function deserialize(string $raw): mixed + { + return ($this->deserializer)($raw); + } + + /** + * @return Generator + */ + private function iterateSse(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield ($this->deserializer)($raw['data']); + } + } + + /** + * Iterates the SSE stream yielding raw envelopes with WHATWG metadata fields + * intact. Yields plain associative arrays — not `SseEvent` objects — so the + * data-only iteration path doesn't pay the allocation cost; `SseStream::events()` + * constructs the public `SseEvent` on top. + * + * Per WHATWG: the `id:` field persists across events within this iteration; + * the configured `terminator`, if present, ends iteration when matched against + * `data`. + * + * @internal + * @return Generator + */ + protected function iterateRawSseEvents(): Generator + { + $dataBuffer = ''; + $eventType = ''; + $lastEventId = ''; + $retry = null; + foreach ($this->readLines() as $line) { + if ($line === '') { + if ($dataBuffer === '') { + continue; + } + $payload = substr($dataBuffer, 0, -1); + $dataBuffer = ''; + if ($this->terminator !== null && $payload === $this->terminator) { + return; + } + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + // Per WHATWG: do NOT reset lastEventId between events. + $eventType = ''; + $retry = null; + continue; + } + if (str_starts_with($line, ':')) { + continue; + } + $colonPos = strpos($line, ':'); + if ($colonPos === false) { + if ($line === 'data') { + $dataBuffer = $this->appendWithinCap($dataBuffer, "\n"); + } + continue; + } + $field = substr($line, 0, $colonPos); + $value = substr($line, $colonPos + 1); + if (str_starts_with($value, ' ')) { + $value = substr($value, 1); + } + switch ($field) { + case 'data': + $dataBuffer = $this->appendWithinCap($dataBuffer, $value . "\n"); + break; + case 'event': + $eventType = $value; + break; + case 'id': + // WHATWG: ignore IDs that contain a NULL byte. + if (!str_contains($value, "\0")) { + $lastEventId = $value; + } + break; + case 'retry': + // WHATWG: ignore the value if it isn't a base-10 integer. + if ($value !== '' && ctype_digit($value)) { + $retry = (int) $value; + } + break; + } + } + // Flush a trailing event that lacked a closing blank line. + if ($dataBuffer !== '') { + $payload = substr($dataBuffer, 0, -1); + if ($this->terminator === null || $payload !== $this->terminator) { + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + } + } + } + + /** + * @return Generator + */ + private function iterateDelimited(): Generator + { + foreach ($this->readLines() as $line) { + if ($line === '') { + continue; + } + if ($this->terminator !== null && $line === $this->terminator) { + return; + } + yield ($this->deserializer)($line); + } + } + + /** + * @return Generator + */ + private function iterateText(): Generator + { + foreach ($this->readLines() as $line) { + yield ($this->deserializer)($line); + } + } + + /** + * Reads the response body and yields complete lines, normalizing CRLF/CR + * to LF per the WHATWG SSE spec. Strips a single UTF-8 BOM if present at + * the start of the stream (WHATWG §9.2.4). Trailing partial content + * (without a terminating newline) is emitted as a final line. + * + * `$pendingCr` tracks whether the prior chunk's last byte was `\r`, so a + * `\r\n` sequence split across a read boundary collapses to one terminator + * instead of two. + * + * @return Generator + */ + private function readLines(): Generator + { + $buffer = ''; + $pendingCr = false; + $bomChecked = false; + while (!$this->body->eof()) { + $chunk = $this->body->read(self::READ_CHUNK_SIZE); + if ($chunk === '') { + continue; + } + if ($pendingCr && $chunk[0] === "\n") { + $chunk = substr($chunk, 1); + } + if (str_contains($chunk, "\r")) { + $pendingCr = str_ends_with($chunk, "\r"); + $chunk = str_replace(["\r\n", "\r"], "\n", $chunk); + } else { + $pendingCr = false; + } + $buffer = $this->appendWithinCap($buffer, $chunk); + if (!$bomChecked && strlen($buffer) >= 3) { + $bomChecked = true; + if (str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + } + while (($lfPos = strpos($buffer, "\n")) !== false) { + yield substr($buffer, 0, $lfPos); + $buffer = substr($buffer, $lfPos + 1); + } + } + // BOM may not have been checked yet if the entire body was < 3 bytes. + if (!$bomChecked && str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + if ($buffer !== '') { + yield $buffer; + } + } + + /** + * Appends $suffix to $buffer, throwing `RuntimeException` if the resulting + * size would exceed `maxBufferSize`. + */ + private function appendWithinCap(string $buffer, string $suffix): string + { + if (strlen($buffer) + strlen($suffix) > $this->maxBufferSize) { + throw new RuntimeException( + "Stream buffer would exceed maximum size of {$this->maxBufferSize} bytes", + ); + } + return $buffer . $suffix; + } + + public function __destruct() + { + try { + $this->body->close(); + } catch (\Throwable) { + // Best effort — the body may already be closed by the consumer. + } + } +} diff --git a/generators/php/base/src/asIs/Client/StreamFormat.Template.php b/generators/php/base/src/asIs/Client/StreamFormat.Template.php new file mode 100644 index 000000000000..d32255df792f --- /dev/null +++ b/generators/php/base/src/asIs/Client/StreamFormat.Template.php @@ -0,0 +1,13 @@ +; + +/** + * Framing strategy used by `Stream` to interpret a streaming HTTP response body. + */ +enum StreamFormat: string +{ + case Sse = 'sse'; + case Json = 'json'; + case Text = 'text'; +} diff --git a/generators/php/base/src/asIs/Client/StreamTest.Template.php b/generators/php/base/src/asIs/Client/StreamTest.Template.php new file mode 100644 index 000000000000..9bf9818ddf67 --- /dev/null +++ b/generators/php/base/src/asIs/Client/StreamTest.Template.php @@ -0,0 +1,297 @@ +; + +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; +use <%= coreNamespace%>\Client\JsonStream; +use <%= coreNamespace%>\Client\SseEvent; +use <%= coreNamespace%>\Client\SseStream; +use <%= coreNamespace%>\Client\Stream; +use <%= coreNamespace%>\Client\TextStream; + +class StreamTest extends TestCase +{ + public function testSseParsesSingleEvent(): void + { + $body = "data: hello\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseConcatenatesMultilineDataWithNewlines(): void + { + $body = "data: line one\ndata: line two\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["line one\nline two"], iterator_to_array($stream, false)); + } + + public function testSseStripsLeadingSpaceFromFieldValues(): void + { + // SSE spec: a single leading space in the field value is stripped. + $body = "data: with-leading-space\ndata:no-leading-space\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["with-leading-space\nno-leading-space"], iterator_to_array($stream, false)); + } + + public function testSseIgnoresCommentLines(): void + { + $body = ": this is a comment\ndata: payload\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['payload'], iterator_to_array($stream, false)); + } + + public function testSseTerminatorEndsIterationCleanly(): void + { + $body = "data: first\n\ndata: [DONE]\n\ndata: never-yielded\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, '[DONE]'); + + $this->assertSame(['first'], iterator_to_array($stream, false)); + } + + public function testSseNormalizesCrlfAndLoneCr(): void + { + $body = "data: a\r\n\r\ndata: b\rdata: c\r\r"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['a', "b\nc"], iterator_to_array($stream, false)); + } + + public function testSseDispatchesTrailingEventWithoutBlankLine(): void + { + $body = "data: incomplete"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['incomplete'], iterator_to_array($stream, false)); + } + + public function testSseAppliesDeserializerOncePerEvent(): void + { + $body = "data: {\"n\":1}\n\ndata: {\"n\":2}\n\n"; + $stream = new SseStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2]], iterator_to_array($stream, false)); + } + + public function testSseEventsExposesEventIdAndRetryMetadata(): void + { + $body = "event: chat\nid: msg-1\nretry: 5000\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertCount(1, $events); + $this->assertInstanceOf(SseEvent::class, $events[0]); + $this->assertSame('hi', $events[0]->data); + $this->assertSame('chat', $events[0]->event); + $this->assertSame('msg-1', $events[0]->id); + $this->assertSame(5000, $events[0]->retry); + } + + public function testSseEventsPersistsLastEventIdAcrossEventsPerSpec(): void + { + // Per WHATWG SSE: once an `id:` is set it persists across subsequent + // events until explicitly overridden. + $body = "id: persistent\ndata: a\n\ndata: b\n\nid: replaced\ndata: c\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['persistent', 'persistent', 'replaced'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresIdContainingNullByte(): void + { + $body = "id: ok\ndata: first\n\nid: bad\0id\ndata: second\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['ok', 'ok'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresNonIntegerRetry(): void + { + $body = "retry: not-a-number\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertNull($events[0]->retry); + } + + public function testSseConstructorRejectsNonSseContentType(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/text\/event-stream/'); + + new SseStream( + self::response('data: x\n\n', contentType: 'application/json'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorRejectsNonUtf8Charset(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/charset/i'); + + new SseStream( + self::response('data: x\n\n', contentType: 'text/event-stream; charset=iso-8859-1'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorAcceptsUtf8CharsetParameter(): void + { + $stream = new SseStream( + self::response("data: hi\n\n", contentType: 'text/event-stream; charset=UTF-8'), + fn (string $d): string => $d, + null, + ); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testSseConstructorToleratesMissingContentTypeHeader(): void + { + $response = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withBody(\Http\Discovery\Psr17FactoryDiscovery::findStreamFactory()->createStream("data: hi\n\n")); + + $stream = new SseStream($response, fn (string $d): string => $d, null); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testStreamThrowsWhenLineBufferExceedsMaxSize(): void + { + $bigLine = str_repeat('A', 200) . "\n"; + $stream = new SseStream( + self::response("data: " . $bigLine . "\n"), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testStreamThrowsBeforeAccumulatingPastMaxBufferOnLongRunningSseEvent(): void + { + // Each line is well under the 64-byte cap; the cumulative `data:` + // append must trip the check before the buffer balloons. + $manyDataLines = ''; + for ($i = 0; $i < 50; $i++) { + $manyDataLines .= "data: chunk-$i\n"; + } + $stream = new SseStream( + self::response($manyDataLines), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testSseStripsUtf8BomFromStartOfStream(): void + { + // WHATWG §9.2.4 requires a leading U+FEFF to be dropped. + $body = "\xEF\xBB\xBFdata: hello\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseStripsBomOnlyAtVeryStart(): void + { + $body = "data: first\n\ndata: \xEF\xBB\xBFsecond\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['first', "\xEF\xBB\xBFsecond"], iterator_to_array($stream, false)); + } + + public function testJsonStreamYieldsOnePerLine(): void + { + $body = "{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2], ['n' => 3]], iterator_to_array($stream, false)); + } + + public function testJsonStreamSkipsEmptyLines(): void + { + $body = "{\"a\":1}\n\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['a' => 1], ['a' => 2]], iterator_to_array($stream, false)); + } + + public function testJsonStreamTerminatorEndsIteration(): void + { + $body = "{\"a\":1}\n[DONE]\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), '[DONE]'); + + $this->assertSame([['a' => 1]], iterator_to_array($stream, false)); + } + + /** + * @return \Closure(string): array + */ + private static function jsonDecoder(): \Closure + { + return static function (string $raw): array { + /** @var array $decoded */ + $decoded = json_decode($raw, true); + return $decoded; + }; + } + + public function testTextStreamYieldsRawLines(): void + { + $body = "alpha\nbeta\ngamma\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', 'beta', 'gamma'], iterator_to_array($stream, false)); + } + + public function testTextStreamPreservesEmptyLines(): void + { + $body = "alpha\n\nbeta\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', '', 'beta'], iterator_to_array($stream, false)); + } + + /** + * Build a PSR-7 ResponseInterface with the given body. The Content-Type + * defaults to `text/event-stream` so SSE tests just work; pass an override + * for the validation-error cases. + */ + private static function response( + string $body, + string $contentType = 'text/event-stream', + ): ResponseInterface { + return \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withHeader('Content-Type', $contentType) + ->withBody( + \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory() + ->createStream($body), + ); + } +} diff --git a/generators/php/base/src/asIs/Client/TextStream.Template.php b/generators/php/base/src/asIs/Client/TextStream.Template.php new file mode 100644 index 000000000000..de562ded9159 --- /dev/null +++ b/generators/php/base/src/asIs/Client/TextStream.Template.php @@ -0,0 +1,30 @@ +; + +use Psr\Http\Message\ResponseInterface; + +/** + * Iterates a streaming text response body, yielding one raw string per line. + * + * @extends Stream + */ +class TextStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: fn (string $line): string => $line, + format: StreamFormat::Text, + terminator: null, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/generators/php/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap b/generators/php/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap index c1d474eec88f..2995f33be12d 100644 --- a/generators/php/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap +++ b/generators/php/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap @@ -388,7 +388,7 @@ exports[`snippets (default) > imdb > 'POST /movies/create-movie (simple)' 1`] = namespace Example; use Acme\\AcmeClient; -use Acme\\Imdb\\Types\\CreateMovieRequest; +use Acme\\Imdb\\Requests\\CreateMovieRequest; $client = new AcmeClient( token: '', diff --git a/generators/php/sdk/changes/2.10.0/add-sse-streaming-support.yml b/generators/php/sdk/changes/2.10.0/add-sse-streaming-support.yml new file mode 100644 index 000000000000..d116352d805d --- /dev/null +++ b/generators/php/sdk/changes/2.10.0/add-sse-streaming-support.yml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Support streaming response bodies in generated PHP SDKs (Server-Sent Events, + NDJSON, and raw text). Endpoints with a `response-stream` now return a typed + `SseStream`, `JsonStream`, or `TextStream` that iterates the response + frame-by-frame and deserializes each one into the declared payload type. + Previously these endpoints were emitted as broken `void` methods that always + threw `ApiException` regardless of HTTP status. + type: feat diff --git a/generators/php/sdk/src/SdkGeneratorContext.ts b/generators/php/sdk/src/SdkGeneratorContext.ts index 39c5a88c7d4b..24a960da491d 100644 --- a/generators/php/sdk/src/SdkGeneratorContext.ts +++ b/generators/php/sdk/src/SdkGeneratorContext.ts @@ -222,6 +222,29 @@ export class SdkGeneratorContext extends AbstractPhpGeneratorContext this.context.logger.error("Streaming not supported"), + streaming: (streamingResponse) => { + streamingResponse._visit({ + sse: (sseChunk) => { + const payloadType = this.context.phpTypeMapper.convert({ reference: sseChunk.payload }); + this.writeStreamingReturn({ + writer, + streamClassReference: this.context.getSseStreamClassReference(payloadType), + payloadType, + terminator: sseChunk.terminator + }); + }, + json: (jsonChunk) => { + const payloadType = this.context.phpTypeMapper.convert({ reference: jsonChunk.payload }); + this.writeStreamingReturn({ + writer, + streamClassReference: this.context.getJsonStreamClassReference(payloadType), + payloadType, + terminator: jsonChunk.terminator + }); + }, + text: () => { + writer.controlFlow( + "if", + php.codeblock( + `${STATUS_CODE_VARIABLE_NAME} >= 200 && ${STATUS_CODE_VARIABLE_NAME} < 400` + ) + ); + writer.write("return "); + writer.writeNodeStatement( + php.instantiateClass({ + classReference: this.context.getTextStreamClassReference(), + arguments_: [ + { + name: "response", + assignment: php.codeblock(RESPONSE_VARIABLE_NAME) + } + ] + }) + ); + writer.endControlFlow(); + }, + _other: () => undefined + }); + }, text: () => { writer.controlFlow( "if", @@ -774,6 +817,115 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { }); } + private writeStreamingReturn({ + writer, + streamClassReference, + payloadType, + terminator + }: { + writer: php.Writer; + streamClassReference: php.ClassReference; + payloadType: php.Type; + terminator: string | undefined; + }): void { + writer.controlFlow( + "if", + php.codeblock(`${STATUS_CODE_VARIABLE_NAME} >= 200 && ${STATUS_CODE_VARIABLE_NAME} < 400`) + ); + const deserializerVarName = "$data"; + const deserializerBody = this.buildStreamDeserializerBody({ + payloadType, + variableName: deserializerVarName + }); + const terminatorLiteral = terminator != null ? `'${terminator.replace(/'/g, "\\'")}'` : "null"; + writer.write("return "); + writer.writeNodeStatement( + php.instantiateClass({ + classReference: streamClassReference, + arguments_: [ + { + name: "response", + assignment: php.codeblock(RESPONSE_VARIABLE_NAME) + }, + { + name: "deserializer", + assignment: php.codeblock((w) => { + w.write(`fn(string ${deserializerVarName}) => `); + w.writeNode(deserializerBody); + }) + }, + { + name: "terminator", + assignment: php.codeblock(terminatorLiteral) + } + ] + }) + ); + writer.endControlFlow(); + } + + /** + * Builds the expression that deserializes a single stream frame's raw string into + * the typed payload. Mirrors the dispatch in decodeJsonResponse, but emits a bare + * expression (no statement terminator) suitable for an arrow-function body. + */ + private buildStreamDeserializerBody({ + payloadType, + variableName + }: { + payloadType: php.Type; + variableName: string; + }): php.CodeBlock { + const internalType = payloadType.underlyingType().internalType; + const argument: UnnamedArgument[] = [php.codeblock(variableName)]; + switch (internalType.type) { + case "reference": + return php.codeblock((writer) => { + writer.writeNode( + php.invokeMethod({ + on: internalType.value, + method: "fromJson", + arguments_: argument, + static_: true + }) + ); + }); + case "int": + case "float": + case "string": + case "bool": + case "date": + case "dateTime": + case "mixed": + case "literal": + case "enumString": { + const methodSuffix = internalType.type === "enumString" ? "String" : upperFirst(internalType.type); + return php.codeblock((writer) => { + writer.writeNode( + php.invokeMethod({ + on: this.context.getJsonDecoderClassReference(), + method: `decode${methodSuffix}`, + arguments_: argument, + static_: true + }) + ); + }); + } + case "array": + case "map": + case "union": + case "object": + case "optional": + case "null": + case "typeDict": + throw GeneratorError.internalError( + `Internal error; '${internalType.type}' type is not a supported streaming payload type` + ); + default: + assertNever(internalType); + } + } + private decodeJsonResponse(return_: php.Type | undefined): php.CodeBlock { if (return_ == null) { return php.codeblock(""); diff --git a/generators/php/sdk/src/endpoint/utils/getEndpointReturnType.ts b/generators/php/sdk/src/endpoint/utils/getEndpointReturnType.ts index 2dc23a920153..6306ec0f3506 100644 --- a/generators/php/sdk/src/endpoint/utils/getEndpointReturnType.ts +++ b/generators/php/sdk/src/endpoint/utils/getEndpointReturnType.ts @@ -21,7 +21,19 @@ export function getEndpointReturnType({ const type = context.phpTypeMapper.convert({ reference: reference.responseBodyType }); return type.isOptional() ? type : php.Type.optional(type); }, - streaming: () => undefined, + streaming: (streamingResponse) => + streamingResponse._visit({ + sse: (sseChunk) => { + const eventType = context.phpTypeMapper.convert({ reference: sseChunk.payload }); + return php.Type.reference(context.getSseStreamClassReference(eventType)); + }, + json: (jsonChunk) => { + const chunkType = context.phpTypeMapper.convert({ reference: jsonChunk.payload }); + return php.Type.reference(context.getJsonStreamClassReference(chunkType)); + }, + text: () => php.Type.reference(context.getTextStreamClassReference()), + _other: () => undefined + }), text: () => php.Type.string(), _other: () => undefined }); diff --git a/generators/php/sdk/versions.yml b/generators/php/sdk/versions.yml index 053c57e8c6cd..df7224e7179c 100644 --- a/generators/php/sdk/versions.yml +++ b/generators/php/sdk/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.10.0 + changelogEntry: + - summary: | + Support streaming response bodies in generated PHP SDKs (Server-Sent Events, + NDJSON, and raw text). Endpoints with a `response-stream` now return a typed + `SseStream`, `JsonStream`, or `TextStream` that iterates the response + frame-by-frame and deserializes each one into the declared payload type. + Previously these endpoints were emitted as broken `void` methods that always + threw `ApiException` regardless of HTTP status. + type: feat + createdAt: "2026-05-14" + irVersion: 66 - version: 2.9.7 changelogEntry: - summary: | diff --git a/generators/python/sdk/changes/5.12.8/cve-2026-34591-poetry-bump.yml b/generators/python/sdk/changes/5.12.8/cve-2026-34591-poetry-bump.yml index 3deea9a3d320..a0f3a490a7d3 100644 --- a/generators/python/sdk/changes/5.12.8/cve-2026-34591-poetry-bump.yml +++ b/generators/python/sdk/changes/5.12.8/cve-2026-34591-poetry-bump.yml @@ -2,7 +2,7 @@ - summary: | Bump Poetry from 1.8.5 to 2.4.1 in the python-sdk and pydantic-model - container images. Clears CVE-2026-34591 (Poetry <2.3.3 stored credentials + container images. Clears CVE-2026-34591 (Poetry `<2.3.3` stored credentials in cleartext when keyring storage was unavailable). pyproject.toml's `poetry-core` constraint moves from `^1.9.0` to `^2.0.0` to stay in lockstep with Poetry 2.4.1's bundled `poetry-core 2.4.0` under diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 1d723f07e1e3..f015caa0bfd5 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -3,7 +3,7 @@ changelogEntry: - summary: | Bump Poetry from 1.8.5 to 2.4.1 in the python-sdk and pydantic-model - container images. Clears CVE-2026-34591 (Poetry <2.3.3 stored credentials + container images. Clears CVE-2026-34591 (Poetry `<2.3.3` stored credentials in cleartext when keyring storage was unavailable). pyproject.toml's `poetry-core` constraint moves from `^1.9.0` to `^2.0.0` to stay in lockstep with Poetry 2.4.1's bundled `poetry-core 2.4.0` under diff --git a/generators/swift/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap b/generators/swift/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap index 565addc5a70d..d21631d7a075 100644 --- a/generators/swift/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap +++ b/generators/swift/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap @@ -333,7 +333,7 @@ import Acme private func main() async throws { let client = AcmeClient(token: "") - _ = try await client.imdb.createMovie(request: CreateMovieRequest( + _ = try await client.imdb.createMovie(request: .init( title: "The Matrix", rating: 8.2 )) diff --git a/generators/typescript-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap b/generators/typescript-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap index f05321f8d97b..6575d9480d22 100644 --- a/generators/typescript-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap +++ b/generators/typescript-v2/dynamic-snippets/src/__test__/__snapshots__/DynamicSnippetsGenerator.test.ts.snap @@ -318,7 +318,9 @@ async function main() { const client = new AcmeClient({ token: "", }); - await client.imdb.getMovie("movie_xyz"); + await client.imdb.getMovie({ + movieID: "movie_xyz", + }); } main(); " diff --git a/packages/cli/cli-v2/src/sdk/generator/LegacyRemoteGenerationRunner.ts b/packages/cli/cli-v2/src/sdk/generator/LegacyRemoteGenerationRunner.ts index 4793c73bdb3a..c1932c641faf 100644 --- a/packages/cli/cli-v2/src/sdk/generator/LegacyRemoteGenerationRunner.ts +++ b/packages/cli/cli-v2/src/sdk/generator/LegacyRemoteGenerationRunner.ts @@ -137,6 +137,7 @@ export class LegacyRemoteGenerationRunner { dynamicIrOnly: false, retryRateLimited: false, requireEnvVars: args.requireEnvVars ?? true, + verify: false, disableTelemetry: !this.context.telemetry.isTelemetryEnabled() }); diff --git a/packages/cli/cli/changes/5.26.0/add-docs-check-links.yml b/packages/cli/cli/changes/5.26.0/add-docs-check-links.yml index a7a825a09d66..4e35db73e13e 100644 --- a/packages/cli/cli/changes/5.26.0/add-docs-check-links.yml +++ b/packages/cli/cli/changes/5.26.0/add-docs-check-links.yml @@ -1,7 +1,7 @@ - summary: | Add `fern docs link check` command to validate links on live documentation sites. Supports text, JSON, and CSV output formats via `--output` flag. - Use `--url ` to specify which docs site to check, or auto-detect from fern.yml. + Use `--url ` to specify which docs site to check, or auto-detect from docs.yml. type: feat - summary: | Add progress bars matching `fern docs dev` style for `fern docs link check`. diff --git a/packages/cli/cli/changes/5.26.1/forward-verify-to-fiddle.yml b/packages/cli/cli/changes/5.26.1/forward-verify-to-fiddle.yml new file mode 100644 index 000000000000..b7aad572bd5d --- /dev/null +++ b/packages/cli/cli/changes/5.26.1/forward-verify-to-fiddle.yml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Forward `--verify` through the remote (Fiddle) generation path. Previously the + CLI-level `--verify` flag only worked for local generation; on remote runs the + value was silently dropped before reaching `CreateJobRequestV2.verify`. The + flag now plumbs through `runRemoteGenerationForAPIWorkspace` → + `runRemoteGenerationForGenerator` → `createAndStartJob` and is set on the + Fiddle job request, enabling the generator-cli pipeline's VerificationStep + against the language-specific validator on opted-in runs. + type: fix diff --git a/packages/cli/cli/changes/5.26.2/fix-commit-author-attribution.yml b/packages/cli/cli/changes/5.26.2/fix-commit-author-attribution.yml new file mode 100644 index 000000000000..f78418814552 --- /dev/null +++ b/packages/cli/cli/changes/5.26.2/fix-commit-author-attribution.yml @@ -0,0 +1,5 @@ +- summary: | + Fix commit author attribution for GitHub Enterprise: API-created commits now + use the Fern bot identity instead of the PAT-owning user, matching the git CLI + behavior of Fern 3.x generators. + type: fix diff --git a/packages/cli/cli/changes/5.26.2/fix-local-docker-venus-auth.yml b/packages/cli/cli/changes/5.26.2/fix-local-docker-venus-auth.yml new file mode 100644 index 000000000000..0396035a401e --- /dev/null +++ b/packages/cli/cli/changes/5.26.2/fix-local-docker-venus-auth.yml @@ -0,0 +1,7 @@ +- summary: | + Authenticate Venus calls during local Docker generation (`fern generate --local`) + by silently picking up an existing `FERN_TOKEN` env var or saved login token, + matching the remote generation path. Previously, `useLocalDocker` skipped the + auth flow entirely, leaving Venus calls (e.g. `GET /organizations/{org_id}`) + unauthenticated. + type: fix diff --git a/packages/cli/cli/changes/5.26.3/fix-global-theme-token-in-dev.yml b/packages/cli/cli/changes/5.26.3/fix-global-theme-token-in-dev.yml new file mode 100644 index 000000000000..9afeca22ffdf --- /dev/null +++ b/packages/cli/cli/changes/5.26.3/fix-global-theme-token-in-dev.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Fix `fern docs dev` grabbing the local fern token for authentication when loading a global theme + type: fix diff --git a/packages/cli/cli/changes/5.26.4/fix-missing-redirects-init-failures-respect-severity.yml b/packages/cli/cli/changes/5.26.4/fix-missing-redirects-init-failures-respect-severity.yml new file mode 100644 index 000000000000..1ee9f8065f2b --- /dev/null +++ b/packages/cli/cli/changes/5.26.4/fix-missing-redirects-init-failures-respect-severity.yml @@ -0,0 +1,10 @@ +- summary: | + Fix `missing-redirects` causing `fern check` to exit with code 1 even when + the rule is configured at `warn`. Rule initialization failures now honor + the configured severity (`warn` emits a warning, `error` emits an error) + instead of always being reported as fatal. The `missing-redirects` rule + also degrades to a warning when the local docs navigation fails to + resolve, captures the underlying `failAndThrow` message so the warning + explains *why* (e.g. `Folder not found: ...`) instead of `[object Object]`, + and non-`Error` throws are formatted readably across the validator. + type: fix diff --git a/packages/cli/cli/src/commands/generate/generateAPIWorkspace.ts b/packages/cli/cli/src/commands/generate/generateAPIWorkspace.ts index c320f9b3ffdf..893edea2b335 100644 --- a/packages/cli/cli/src/commands/generate/generateAPIWorkspace.ts +++ b/packages/cli/cli/src/commands/generate/generateAPIWorkspace.ts @@ -198,6 +198,7 @@ export async function generateWorkspace({ occurrenceTracker, skipIfNoDiff, noReplay, + verify, disableTelemetry: isTelemetryDisabled() }); } diff --git a/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts b/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts index 4b9be07f104b..539acc8ce885 100644 --- a/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate/generateAPIWorkspaces.ts @@ -1,4 +1,4 @@ -import { createOrganizationIfDoesNotExist, FernToken } from "@fern-api/auth"; +import { createOrganizationIfDoesNotExist, FernToken, getToken } from "@fern-api/auth"; import { ContainerRunner, Values } from "@fern-api/core-utils"; import { AbsoluteFilePath, cwd, join, RelativeFilePath, resolve } from "@fern-api/fs-utils"; import { askToLogin } from "@fern-api/login"; @@ -100,6 +100,11 @@ export async function generateAPIWorkspaces({ }); } token = currentToken; + } else { + // Local generation must stay non-interactive: silently pick up an existing + // token (FERN_TOKEN env var or saved login file) so Venus calls are + // authenticated when possible, and leave `token` undefined otherwise. + token = await getToken(); } await confirmOutputDirectoriesForEligibleGenerators({ diff --git a/packages/cli/cli/src/commands/sdk-preview/sdkPreview.ts b/packages/cli/cli/src/commands/sdk-preview/sdkPreview.ts index 721ad5f05322..0d1e753ac2e9 100644 --- a/packages/cli/cli/src/commands/sdk-preview/sdkPreview.ts +++ b/packages/cli/cli/src/commands/sdk-preview/sdkPreview.ts @@ -298,7 +298,8 @@ export async function sdkPreview({ dynamicIrOnly: false, validateWorkspace: true, retryRateLimited: false, - requireEnvVars: false + requireEnvVars: false, + verify: false }; // Job 1: Publish preview package to registry. diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index 93789bce72de..ed54e56c52cc 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,10 +1,60 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 5.26.4 + changelogEntry: + - summary: | + Fix `missing-redirects` causing `fern check` to exit with code 1 even when + the rule is configured at `warn`. Rule initialization failures now honor + the configured severity (`warn` emits a warning, `error` emits an error) + instead of always being reported as fatal. The `missing-redirects` rule + also degrades to a warning when the local docs navigation fails to + resolve, captures the underlying `failAndThrow` message so the warning + explains *why* (e.g. `Folder not found: ...`) instead of `[object Object]`, + and non-`Error` throws are formatted readably across the validator. + type: fix + createdAt: "2026-05-14" + irVersion: 66 +- version: 5.26.3 + changelogEntry: + - summary: | + Fix `fern docs dev` grabbing the local fern token for authentication when loading a global theme + type: fix + createdAt: "2026-05-14" + irVersion: 66 +- version: 5.26.2 + changelogEntry: + - summary: | + Fix commit author attribution for GitHub Enterprise: API-created commits now + use the Fern bot identity instead of the PAT-owning user, matching the git CLI + behavior of Fern 3.x generators. + type: fix + - summary: | + Authenticate Venus calls during local Docker generation (`fern generate --local`) + by silently picking up an existing `FERN_TOKEN` env var or saved login token, + matching the remote generation path. Previously, `useLocalDocker` skipped the + auth flow entirely, leaving Venus calls (e.g. `GET /organizations/{org_id}`) + unauthenticated. + type: fix + createdAt: "2026-05-14" + irVersion: 66 +- version: 5.26.1 + changelogEntry: + - summary: | + Forward `--verify` through the remote (Fiddle) generation path. Previously the + CLI-level `--verify` flag only worked for local generation; on remote runs the + value was silently dropped before reaching `CreateJobRequestV2.verify`. The + flag now plumbs through `runRemoteGenerationForAPIWorkspace` → + `runRemoteGenerationForGenerator` → `createAndStartJob` and is set on the + Fiddle job request, enabling the generator-cli pipeline's VerificationStep + against the language-specific validator on opted-in runs. + type: fix + createdAt: "2026-05-14" + irVersion: 66 - version: 5.26.0 changelogEntry: - summary: | Add `fern docs link check` command to validate links on live documentation sites. Supports text, JSON, and CSV output formats via `--output` flag. - Use `--url ` to specify which docs site to check, or auto-detect from fern.yml. + Use `--url ` to specify which docs site to check, or auto-detect from docs.yml. type: feat - summary: | Add progress bars matching `fern docs dev` style for `fern docs link check`. diff --git a/packages/cli/docs-preview/package.json b/packages/cli/docs-preview/package.json index 4ce46249e824..c9b5629c4a76 100644 --- a/packages/cli/docs-preview/package.json +++ b/packages/cli/docs-preview/package.json @@ -32,6 +32,7 @@ "test:update": "vitest --run -u" }, "dependencies": { + "@fern-api/auth": "workspace:*", "@fern-api/core-utils": "workspace:*", "@fern-api/docs-markdown-utils": "workspace:*", "@fern-api/docs-resolver": "workspace:*", diff --git a/packages/cli/docs-preview/src/previewDocs.ts b/packages/cli/docs-preview/src/previewDocs.ts index d7ca7b1f8688..31933e36f945 100644 --- a/packages/cli/docs-preview/src/previewDocs.ts +++ b/packages/cli/docs-preview/src/previewDocs.ts @@ -1,3 +1,4 @@ +import { getUserToken } from "@fern-api/auth"; import { extractErrorMessage, replaceEnvVariables } from "@fern-api/core-utils"; import { isValidRelativeSlug, @@ -419,7 +420,9 @@ async function applyGlobalThemeIfNeeded( if (themeName == null) { return docsWorkspace; } - const token = process.env.FERN_TOKEN; + // Prefer the stored fern login token; fall back to env var (used in CI). + const storedToken = await getUserToken(); + const token = storedToken?.value ?? process.env.FERN_TOKEN; if (token == null) { context.logger.warn( `docs.yml declares global-theme "${themeName}" but FERN_TOKEN is not set — ` + diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/imdb/type__Movie.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/imdb/type__Movie.json new file mode 100644 index 000000000000..e9502799e9ed --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/imdb/type__Movie.json @@ -0,0 +1,25 @@ +{ + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/MovieId" + }, + "title": { + "type": "string" + }, + "rating": { + "type": "number" + } + }, + "required": [ + "id", + "title", + "rating" + ], + "additionalProperties": false, + "definitions": { + "MovieId": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/imdb/type__MovieId.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/imdb/type__MovieId.json new file mode 100644 index 000000000000..cdb8c887b62c --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/imdb/type__MovieId.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json index 892a42c4e1da..e5e3a56cd114 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/imdb.json @@ -1,7 +1,7 @@ { "version": "1.0.0", "types": { - "type_imdb:MovieId": { + "type_:MovieId": { "type": "alias", "declaration": { "name": { @@ -24,47 +24,9 @@ } }, "fernFilepath": { - "allParts": [ - { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - ], + "allParts": [], "packagePath": [], - "file": { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } + "file": null } }, "typeReference": { @@ -72,7 +34,7 @@ "value": "STRING" } }, - "type_imdb:Movie": { + "type_:Movie": { "type": "object", "declaration": { "name": { @@ -95,47 +57,9 @@ } }, "fernFilepath": { - "allParts": [ - { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - ], + "allParts": [], "packagePath": [], - "file": { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } + "file": null } }, "properties": [ @@ -164,7 +88,7 @@ }, "typeReference": { "type": "named", - "value": "type_imdb:MovieId" + "value": "type_:MovieId" }, "propertyAccess": null, "variable": null @@ -232,137 +156,6 @@ ], "extends": null, "additionalProperties": false - }, - "type_imdb:CreateMovieRequest": { - "type": "object", - "declaration": { - "name": { - "originalName": "CreateMovieRequest", - "camelCase": { - "unsafeName": "createMovieRequest", - "safeName": "createMovieRequest" - }, - "snakeCase": { - "unsafeName": "create_movie_request", - "safeName": "create_movie_request" - }, - "screamingSnakeCase": { - "unsafeName": "CREATE_MOVIE_REQUEST", - "safeName": "CREATE_MOVIE_REQUEST" - }, - "pascalCase": { - "unsafeName": "CreateMovieRequest", - "safeName": "CreateMovieRequest" - } - }, - "fernFilepath": { - "allParts": [ - { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - ], - "packagePath": [], - "file": { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - } - }, - "properties": [ - { - "name": { - "wireValue": "title", - "name": { - "originalName": "title", - "camelCase": { - "unsafeName": "title", - "safeName": "title" - }, - "snakeCase": { - "unsafeName": "title", - "safeName": "title" - }, - "screamingSnakeCase": { - "unsafeName": "TITLE", - "safeName": "TITLE" - }, - "pascalCase": { - "unsafeName": "Title", - "safeName": "Title" - } - } - }, - "typeReference": { - "type": "primitive", - "value": "STRING" - }, - "propertyAccess": null, - "variable": null - }, - { - "name": { - "wireValue": "rating", - "name": { - "originalName": "rating", - "camelCase": { - "unsafeName": "rating", - "safeName": "rating" - }, - "snakeCase": { - "unsafeName": "rating", - "safeName": "rating" - }, - "screamingSnakeCase": { - "unsafeName": "RATING", - "safeName": "RATING" - }, - "pascalCase": { - "unsafeName": "Rating", - "safeName": "Rating" - } - } - }, - "typeReference": { - "type": "primitive", - "value": "DOUBLE" - }, - "propertyAccess": null, - "variable": null - } - ], - "extends": null, - "additionalProperties": false } }, "headers": [], @@ -459,14 +252,142 @@ "path": "/movies/create-movie" }, "request": { - "type": "body", + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateMovieRequest", + "camelCase": { + "unsafeName": "createMovieRequest", + "safeName": "createMovieRequest" + }, + "snakeCase": { + "unsafeName": "create_movie_request", + "safeName": "create_movie_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_MOVIE_REQUEST", + "safeName": "CREATE_MOVIE_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateMovieRequest", + "safeName": "CreateMovieRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + ], + "packagePath": [], + "file": { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + } + }, "pathParameters": [], + "queryParameters": [], + "headers": [], "body": { - "type": "typeReference", - "value": { - "type": "named", - "value": "type_imdb:CreateMovieRequest" - } + "type": "properties", + "value": [ + { + "name": { + "wireValue": "title", + "name": { + "originalName": "title", + "camelCase": { + "unsafeName": "title", + "safeName": "title" + }, + "snakeCase": { + "unsafeName": "title", + "safeName": "title" + }, + "screamingSnakeCase": { + "unsafeName": "TITLE", + "safeName": "TITLE" + }, + "pascalCase": { + "unsafeName": "Title", + "safeName": "Title" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "rating", + "name": { + "originalName": "rating", + "camelCase": { + "unsafeName": "rating", + "safeName": "rating" + }, + "snakeCase": { + "unsafeName": "rating", + "safeName": "rating" + }, + "screamingSnakeCase": { + "unsafeName": "RATING", + "safeName": "RATING" + }, + "pascalCase": { + "unsafeName": "Rating", + "safeName": "Rating" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "DOUBLE" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false } }, "response": { @@ -566,7 +487,71 @@ "path": "/movies/{movieId}" }, "request": { - "type": "body", + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetMovieImdbRequest", + "camelCase": { + "unsafeName": "getMovieImdbRequest", + "safeName": "getMovieImdbRequest" + }, + "snakeCase": { + "unsafeName": "get_movie_imdb_request", + "safeName": "get_movie_imdb_request" + }, + "screamingSnakeCase": { + "unsafeName": "GET_MOVIE_IMDB_REQUEST", + "safeName": "GET_MOVIE_IMDB_REQUEST" + }, + "pascalCase": { + "unsafeName": "GetMovieImdbRequest", + "safeName": "GetMovieImdbRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + ], + "packagePath": [], + "file": { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + } + }, "pathParameters": [ { "name": { @@ -593,13 +578,19 @@ }, "typeReference": { "type": "named", - "value": "type_imdb:MovieId" + "value": "type_:MovieId" }, "propertyAccess": null, "variable": null } ], - "body": null + "queryParameters": [], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": true + } }, "response": { "type": "json" diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/literal-user-agent.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/literal-user-agent.json new file mode 100644 index 000000000000..3ec1ab0aed7d --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/literal-user-agent.json @@ -0,0 +1,87 @@ +{ + "version": "1.0.0", + "types": {}, + "headers": [ + { + "name": { + "wireValue": "user-agent", + "name": { + "originalName": "userAgent", + "camelCase": { + "unsafeName": "userAgent", + "safeName": "userAgent" + }, + "snakeCase": { + "unsafeName": "user_agent", + "safeName": "user_agent" + }, + "screamingSnakeCase": { + "unsafeName": "USER_AGENT", + "safeName": "USER_AGENT" + }, + "pascalCase": { + "unsafeName": "UserAgent", + "safeName": "UserAgent" + } + } + }, + "typeReference": { + "type": "literal", + "value": { + "type": "string", + "value": "my-sdk" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "endpoints": { + "endpoint_.ping": { + "auth": null, + "declaration": { + "name": { + "originalName": "ping", + "camelCase": { + "unsafeName": "ping", + "safeName": "ping" + }, + "snakeCase": { + "unsafeName": "ping", + "safeName": "ping" + }, + "screamingSnakeCase": { + "unsafeName": "PING", + "safeName": "PING" + }, + "pascalCase": { + "unsafeName": "Ping", + "safeName": "Ping" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "location": { + "method": "GET", + "path": "/ping" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "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/imdb.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json index e1e1b119a073..68b89f8c0a6f 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/imdb.json @@ -3,7 +3,7 @@ "fdrApiDefinitionId": null, "apiVersion": null, "apiName": "api", - "apiDisplayName": null, + "apiDisplayName": "api", "apiDocs": null, "auth": { "requirement": "ALL", @@ -22,19 +22,17 @@ "headers": [], "idempotencyHeaders": [], "types": { - "type_imdb:MovieId": { + "type_:MovieId": { "inline": null, "name": { "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId" + "typeId": "type_:MovieId" }, "shape": { "_type": "alias", @@ -73,19 +71,17 @@ "availability": null, "docs": null }, - "type_imdb:Movie": { + "type_:Movie": { "inline": null, "name": { "name": "Movie", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:Movie" + "typeId": "type_:Movie" }, "shape": { "_type": "object", @@ -97,14 +93,12 @@ "_type": "named", "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId", + "typeId": "type_:MovieId", "default": null, "inline": null }, @@ -169,7 +163,7 @@ "extendedProperties": [] }, "referencedTypes": [ - "type_imdb:MovieId" + "type_:MovieId" ], "encoding": { "json": {}, @@ -184,122 +178,77 @@ "message": null }, "docs": null - }, - "type_imdb:CreateMovieRequest": { - "inline": null, - "name": { - "name": "CreateMovieRequest", - "fernFilepath": { - "allParts": [ - "imdb" - ], - "packagePath": [], - "file": "imdb" - }, - "displayName": null, - "typeId": "type_imdb:CreateMovieRequest" - }, - "shape": { - "_type": "object", - "extends": [], - "properties": [ - { - "name": "title", - "valueType": { - "_type": "primitive", - "primitive": { - "v1": "STRING", - "v2": { - "type": "string", - "default": null, - "validation": null - } - } - }, - "propertyAccess": null, - "defaultValue": null, - "v2Examples": { - "userSpecifiedExamples": {}, - "autogeneratedExamples": {} - }, - "availability": null, - "docs": null - }, - { - "name": "rating", - "valueType": { - "_type": "primitive", - "primitive": { - "v1": "DOUBLE", - "v2": { - "type": "double", - "default": null, - "validation": null - } - } - }, - "propertyAccess": null, - "defaultValue": 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 } }, "errors": { - "error_imdb:MovieDoesNotExistError": { + "error_:NotFoundError": { "name": { - "name": "MovieDoesNotExistError", + "name": "NotFoundError", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, - "errorId": "error_imdb:MovieDoesNotExistError" + "errorId": "error_:NotFoundError" }, - "discriminantValue": "MovieDoesNotExistError", + "discriminantValue": "NotFoundError", "statusCode": 404, "isWildcardStatusCode": null, "type": { "_type": "named", "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId", + "typeId": "type_:MovieId", "default": null, "inline": null }, - "examples": [], + "examples": [ + { + "name": null, + "shape": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:MovieId", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "MovieId", + "displayName": null + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "jsonExample": "string" + }, + "jsonExample": "string", + "docs": null + } + ], "v2Examples": null, "displayName": null, "headers": [], - "docs": null + "docs": "MovieDoesNotExistError" } }, "services": { @@ -316,7 +265,7 @@ }, "displayName": null, "basePath": { - "head": "/movies", + "head": "", "parts": [] }, "headers": [], @@ -341,11 +290,11 @@ "method": "POST", "basePath": null, "path": { - "head": "/create-movie", + "head": "/movies/create-movie", "parts": [] }, "fullPath": { - "head": "/movies/create-movie", + "head": "movies/create-movie", "parts": [] }, "pathParameters": [], @@ -353,51 +302,69 @@ "queryParameters": [], "headers": [], "requestBody": { - "type": "reference", - "requestBodyType": { - "_type": "named", - "name": "CreateMovieRequest", - "fernFilepath": { - "allParts": [ - "imdb" - ], - "packagePath": [], - "file": "imdb" + "type": "inlinedRequestBody", + "name": "CreateMovieRequest", + "extends": [], + "properties": [ + { + "name": "title", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null }, - "displayName": null, - "typeId": "type_imdb:CreateMovieRequest", - "default": null, - "inline": null - }, + { + "name": "rating", + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + }, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], "docs": null, - "contentType": null, - "v2Examples": null + "v2Examples": null, + "contentType": "application/json" }, "v2RequestBodies": null, "sdkRequest": { "shape": { - "type": "justRequestBody", - "value": { - "type": "typeReference", - "requestBodyType": { - "_type": "named", - "name": "CreateMovieRequest", - "fernFilepath": { - "allParts": [ - "imdb" - ], - "packagePath": [], - "file": "imdb" - }, - "displayName": null, - "typeId": "type_imdb:CreateMovieRequest", - "default": null, - "inline": null - }, - "docs": null, - "contentType": null, - "v2Examples": null - } + "type": "wrapper", + "wrapperName": "CreateMovieRequest", + "bodyKey": "body", + "includePathParameters": false, + "onlyPathParameters": false }, "requestParameterName": "request", "streamParameter": null @@ -411,117 +378,166 @@ "_type": "named", "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId", + "typeId": "type_:MovieId", "default": null, "inline": null }, - "docs": null, + "docs": "Success", "v2Examples": null } }, "status-code": 201, "isWildcardStatusCode": null, - "docs": null + "docs": "Success" }, "v2Responses": null, "errors": [], - "userSpecifiedExamples": [], - "autogeneratedExamples": [ + "userSpecifiedExamples": [ { "example": { - "id": "bd044c94", - "url": "/movies/create-movie", + "id": "29893e03", "name": null, - "endpointHeaders": [], + "url": "/movies/create-movie", + "rootPathParameters": [], "endpointPathParameters": [], - "queryParameters": [], "servicePathParameters": [], + "endpointHeaders": [], "serviceHeaders": [], - "rootPathParameters": [], + "queryParameters": [], "request": { - "type": "reference", - "shape": { - "type": "named", - "shape": { - "type": "object", - "properties": [ - { - "name": "title", - "originalTypeDeclaration": { - "name": "CreateMovieRequest", - "fernFilepath": { - "allParts": [ - "imdb" - ], - "packagePath": [], - "file": "imdb" - }, - "displayName": null, - "typeId": "type_imdb:CreateMovieRequest" + "type": "inlinedRequestBody", + "properties": [ + { + "name": "title", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "title" + } + } + }, + "jsonExample": "title" + }, + "originalTypeDeclaration": null + }, + { + "name": "rating", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "originalTypeDeclaration": null + } + ], + "extraProperties": null, + "jsonExample": { + "title": "title", + "rating": 1.1 + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:MovieId", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null }, + "name": "MovieId", + "displayName": null + }, + "shape": { + "type": "alias", "value": { "shape": { "type": "primitive", "primitive": { "type": "string", "string": { - "original": "title" + "original": "string" } } }, - "jsonExample": "title" - }, - "propertyAccess": null - }, - { - "name": "rating", - "originalTypeDeclaration": { - "name": "CreateMovieRequest", - "fernFilepath": { - "allParts": [ - "imdb" - ], - "packagePath": [], - "file": "imdb" - }, - "displayName": null, - "typeId": "type_imdb:CreateMovieRequest" - }, - "value": { - "shape": { - "type": "primitive", - "primitive": { - "type": "double", - "double": 1.1 - } - }, - "jsonExample": 1.1 - }, - "propertyAccess": null + "jsonExample": "string" + } } - ], - "extraProperties": null - }, - "typeName": { - "name": "CreateMovieRequest", - "fernFilepath": { - "allParts": [ - "imdb" - ], - "packagePath": [], - "file": "imdb" }, - "displayName": null, - "typeId": "type_imdb:CreateMovieRequest" + "jsonExample": "string" } - }, + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "80b0fbf", + "url": "/movies/create-movie", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": "title", + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "title" + } + } + }, + "jsonExample": "title" + } + }, + { + "name": "rating", + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + } + } + ], + "extraProperties": null, "jsonExample": { "title": "title", "rating": 1.1 @@ -552,14 +568,12 @@ "typeName": { "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId" + "typeId": "type_:MovieId" } }, "jsonExample": "string" @@ -579,7 +593,7 @@ "apiPlayground": null, "responseHeaders": [], "availability": { - "status": "PRE_RELEASE", + "status": "BETA", "message": null }, "docs": "Add a movie to the database using the movies/* /... path." @@ -596,7 +610,7 @@ "method": "GET", "basePath": null, "path": { - "head": "/", + "head": "/movies/", "parts": [ { "pathParameter": "movieId", @@ -605,7 +619,7 @@ ] }, "fullPath": { - "head": "/movies/", + "head": "movies/", "parts": [ { "pathParameter": "movieId", @@ -620,14 +634,12 @@ "_type": "named", "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId", + "typeId": "type_:MovieId", "default": null, "inline": null }, @@ -649,14 +661,12 @@ "_type": "named", "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId", + "typeId": "type_:MovieId", "default": null, "inline": null }, @@ -675,7 +685,17 @@ "headers": [], "requestBody": null, "v2RequestBodies": null, - "sdkRequest": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": "GetMovieImdbRequest", + "bodyKey": "body", + "includePathParameters": true, + "onlyPathParameters": true + }, + "requestParameterName": "request", + "streamParameter": null + }, "response": { "body": { "type": "json", @@ -685,57 +705,232 @@ "_type": "named", "name": "Movie", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:Movie", + "typeId": "type_:Movie", "default": null, "inline": null }, - "docs": null, + "docs": "Success", "v2Examples": null } }, - "status-code": null, + "status-code": 200, "isWildcardStatusCode": null, - "docs": null + "docs": "Success" }, "v2Responses": null, "errors": [ { "error": { - "name": "MovieDoesNotExistError", + "name": "NotFoundError", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, - "errorId": "error_imdb:MovieDoesNotExistError" + "errorId": "error_:NotFoundError" }, "docs": null } ], - "userSpecifiedExamples": [], - "autogeneratedExamples": [ + "userSpecifiedExamples": [ { "example": { - "id": "c0924bff", - "url": "/movies/movieId", + "id": "4f9f05f3", "name": null, - "endpointHeaders": [], + "url": "/movies/movieId", + "rootPathParameters": [], "endpointPathParameters": [ { "name": "movieId", "value": { "shape": { "type": "named", - "shape": { + "typeName": { + "typeId": "type_:MovieId", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "MovieId", + "displayName": null + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "movieId" + } + } + }, + "jsonExample": "movieId" + } + } + }, + "jsonExample": "movieId" + } + } + ], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:Movie", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "Movie", + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": "id", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:MovieId", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "MovieId", + "displayName": null + }, + "shape": { + "type": "alias", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + }, + "jsonExample": "id" + }, + "originalTypeDeclaration": { + "typeId": "type_:Movie", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "Movie", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "title", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "title" + } + } + }, + "jsonExample": "title" + }, + "originalTypeDeclaration": { + "typeId": "type_:Movie", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "Movie", + "displayName": null + }, + "propertyAccess": null + }, + { + "name": "rating", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "originalTypeDeclaration": { + "typeId": "type_:Movie", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": "Movie", + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "id": "id", + "title": "title", + "rating": 1.1 + } + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "d95911d9", + "url": "/movies/movieId", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": "movieId", + "value": { + "shape": { + "type": "named", + "shape": { "type": "alias", "value": { "shape": { @@ -753,14 +948,12 @@ "typeName": { "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId" + "typeId": "type_:MovieId" } }, "jsonExample": "movieId" @@ -787,14 +980,12 @@ "originalTypeDeclaration": { "name": "Movie", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:Movie" + "typeId": "type_:Movie" }, "value": { "shape": { @@ -817,14 +1008,12 @@ "typeName": { "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId" + "typeId": "type_:MovieId" } }, "jsonExample": "id" @@ -836,14 +1025,12 @@ "originalTypeDeclaration": { "name": "Movie", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:Movie" + "typeId": "type_:Movie" }, "value": { "shape": { @@ -864,14 +1051,12 @@ "originalTypeDeclaration": { "name": "Movie", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:Movie" + "typeId": "type_:Movie" }, "value": { "shape": { @@ -891,14 +1076,12 @@ "typeName": { "name": "Movie", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:Movie" + "typeId": "type_:Movie" } }, "jsonExample": { @@ -914,7 +1097,7 @@ }, { "example": { - "id": "86c67228", + "id": "28138ef6", "url": "/movies/movieId", "name": null, "endpointHeaders": [], @@ -942,14 +1125,12 @@ "typeName": { "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId" + "typeId": "type_:MovieId" } }, "jsonExample": "movieId" @@ -984,28 +1165,24 @@ "typeName": { "name": "MovieId", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, "displayName": null, - "typeId": "type_imdb:MovieId" + "typeId": "type_:MovieId" } }, "jsonExample": "string" }, "error": { - "name": "MovieDoesNotExistError", + "name": "NotFoundError", "fernFilepath": { - "allParts": [ - "imdb" - ], + "allParts": [], "packagePath": [], - "file": "imdb" + "file": null }, - "errorId": "error_imdb:MovieDoesNotExistError" + "errorId": "error_:NotFoundError" } }, "docs": null @@ -1043,9 +1220,8 @@ "serviceTypeReferenceInfo": { "typesReferencedOnlyByService": { "service_imdb": [ - "type_imdb:MovieId", - "type_imdb:Movie", - "type_imdb:CreateMovieRequest" + "type_:MovieId", + "type_:Movie" ] }, "sharedTypes": [] @@ -1058,7 +1234,7 @@ "dynamic": { "version": "1.0.0", "types": { - "type_imdb:MovieId": { + "type_:MovieId": { "type": "alias", "declaration": { "name": { @@ -1081,47 +1257,9 @@ } }, "fernFilepath": { - "allParts": [ - { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - ], + "allParts": [], "packagePath": [], - "file": { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } + "file": null } }, "typeReference": { @@ -1129,7 +1267,7 @@ "value": "STRING" } }, - "type_imdb:Movie": { + "type_:Movie": { "type": "object", "declaration": { "name": { @@ -1152,47 +1290,9 @@ } }, "fernFilepath": { - "allParts": [ - { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - ], + "allParts": [], "packagePath": [], - "file": { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } + "file": null } }, "properties": [ @@ -1221,7 +1321,7 @@ }, "typeReference": { "type": "named", - "value": "type_imdb:MovieId" + "value": "type_:MovieId" }, "propertyAccess": null, "variable": null @@ -1289,137 +1389,6 @@ ], "extends": null, "additionalProperties": false - }, - "type_imdb:CreateMovieRequest": { - "type": "object", - "declaration": { - "name": { - "originalName": "CreateMovieRequest", - "camelCase": { - "unsafeName": "createMovieRequest", - "safeName": "createMovieRequest" - }, - "snakeCase": { - "unsafeName": "create_movie_request", - "safeName": "create_movie_request" - }, - "screamingSnakeCase": { - "unsafeName": "CREATE_MOVIE_REQUEST", - "safeName": "CREATE_MOVIE_REQUEST" - }, - "pascalCase": { - "unsafeName": "CreateMovieRequest", - "safeName": "CreateMovieRequest" - } - }, - "fernFilepath": { - "allParts": [ - { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - ], - "packagePath": [], - "file": { - "originalName": "imdb", - "camelCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "snakeCase": { - "unsafeName": "imdb", - "safeName": "imdb" - }, - "screamingSnakeCase": { - "unsafeName": "IMDB", - "safeName": "IMDB" - }, - "pascalCase": { - "unsafeName": "Imdb", - "safeName": "Imdb" - } - } - } - }, - "properties": [ - { - "name": { - "wireValue": "title", - "name": { - "originalName": "title", - "camelCase": { - "unsafeName": "title", - "safeName": "title" - }, - "snakeCase": { - "unsafeName": "title", - "safeName": "title" - }, - "screamingSnakeCase": { - "unsafeName": "TITLE", - "safeName": "TITLE" - }, - "pascalCase": { - "unsafeName": "Title", - "safeName": "Title" - } - } - }, - "typeReference": { - "type": "primitive", - "value": "STRING" - }, - "propertyAccess": null, - "variable": null - }, - { - "name": { - "wireValue": "rating", - "name": { - "originalName": "rating", - "camelCase": { - "unsafeName": "rating", - "safeName": "rating" - }, - "snakeCase": { - "unsafeName": "rating", - "safeName": "rating" - }, - "screamingSnakeCase": { - "unsafeName": "RATING", - "safeName": "RATING" - }, - "pascalCase": { - "unsafeName": "Rating", - "safeName": "Rating" - } - } - }, - "typeReference": { - "type": "primitive", - "value": "DOUBLE" - }, - "propertyAccess": null, - "variable": null - } - ], - "extends": null, - "additionalProperties": false } }, "headers": [], @@ -1516,14 +1485,142 @@ "path": "/movies/create-movie" }, "request": { - "type": "body", + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateMovieRequest", + "camelCase": { + "unsafeName": "createMovieRequest", + "safeName": "createMovieRequest" + }, + "snakeCase": { + "unsafeName": "create_movie_request", + "safeName": "create_movie_request" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_MOVIE_REQUEST", + "safeName": "CREATE_MOVIE_REQUEST" + }, + "pascalCase": { + "unsafeName": "CreateMovieRequest", + "safeName": "CreateMovieRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + ], + "packagePath": [], + "file": { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + } + }, "pathParameters": [], + "queryParameters": [], + "headers": [], "body": { - "type": "typeReference", - "value": { - "type": "named", - "value": "type_imdb:CreateMovieRequest" - } + "type": "properties", + "value": [ + { + "name": { + "wireValue": "title", + "name": { + "originalName": "title", + "camelCase": { + "unsafeName": "title", + "safeName": "title" + }, + "snakeCase": { + "unsafeName": "title", + "safeName": "title" + }, + "screamingSnakeCase": { + "unsafeName": "TITLE", + "safeName": "TITLE" + }, + "pascalCase": { + "unsafeName": "Title", + "safeName": "Title" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "wireValue": "rating", + "name": { + "originalName": "rating", + "camelCase": { + "unsafeName": "rating", + "safeName": "rating" + }, + "snakeCase": { + "unsafeName": "rating", + "safeName": "rating" + }, + "screamingSnakeCase": { + "unsafeName": "RATING", + "safeName": "RATING" + }, + "pascalCase": { + "unsafeName": "Rating", + "safeName": "Rating" + } + } + }, + "typeReference": { + "type": "primitive", + "value": "DOUBLE" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false } }, "response": { @@ -1623,7 +1720,71 @@ "path": "/movies/{movieId}" }, "request": { - "type": "body", + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetMovieImdbRequest", + "camelCase": { + "unsafeName": "getMovieImdbRequest", + "safeName": "getMovieImdbRequest" + }, + "snakeCase": { + "unsafeName": "get_movie_imdb_request", + "safeName": "get_movie_imdb_request" + }, + "screamingSnakeCase": { + "unsafeName": "GET_MOVIE_IMDB_REQUEST", + "safeName": "GET_MOVIE_IMDB_REQUEST" + }, + "pascalCase": { + "unsafeName": "GetMovieImdbRequest", + "safeName": "GetMovieImdbRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + ], + "packagePath": [], + "file": { + "originalName": "imdb", + "camelCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "snakeCase": { + "unsafeName": "imdb", + "safeName": "imdb" + }, + "screamingSnakeCase": { + "unsafeName": "IMDB", + "safeName": "IMDB" + }, + "pascalCase": { + "unsafeName": "Imdb", + "safeName": "Imdb" + } + } + } + }, "pathParameters": [ { "name": { @@ -1650,13 +1811,19 @@ }, "typeReference": { "type": "named", - "value": "type_imdb:MovieId" + "value": "type_:MovieId" }, "propertyAccess": null, "variable": null } ], - "body": null + "queryParameters": [], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": true + } }, "response": { "type": "json" @@ -1689,14 +1856,8 @@ "file": "imdb" }, "service": "service_imdb", - "types": [ - "type_imdb:MovieId", - "type_imdb:Movie", - "type_imdb:CreateMovieRequest" - ], - "errors": [ - "error_imdb:MovieDoesNotExistError" - ], + "types": [], + "errors": [], "subpackages": [], "navigationConfig": null, "webhooks": null, @@ -1713,8 +1874,13 @@ }, "websocket": null, "service": null, - "types": [], - "errors": [], + "types": [ + "type_:MovieId", + "type_:Movie" + ], + "errors": [ + "error_:NotFoundError" + ], "subpackages": [ "subpackage_imdb" ], diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/literal-user-agent.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/literal-user-agent.json new file mode 100644 index 000000000000..68ec668d65fe --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/literal-user-agent.json @@ -0,0 +1,314 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "apiName": "literal-user-agent", + "apiDisplayName": null, + "apiDocs": "Test fixture for literal User-Agent header in api.headers config.", + "auth": { + "requirement": "ALL", + "schemes": [], + "docs": null + }, + "headers": [ + { + "name": { + "wireValue": "user-agent", + "name": "userAgent" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "literal", + "literal": { + "type": "string", + "string": "my-sdk" + } + } + }, + "env": null, + "clientDefault": null, + "defaultValue": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "idempotencyHeaders": [], + "types": {}, + "errors": {}, + "services": { + "service_": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_.ping", + "name": "ping", + "displayName": null, + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/ping", + "parts": [] + }, + "fullPath": { + "head": "ping", + "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": "/ping", + "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 + } + }, + "constants": { + "errorInstanceIdKey": "errorInstanceId" + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": {}, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": {}, + "headers": [ + { + "name": { + "wireValue": "user-agent", + "name": { + "originalName": "userAgent", + "camelCase": { + "unsafeName": "userAgent", + "safeName": "userAgent" + }, + "snakeCase": { + "unsafeName": "user_agent", + "safeName": "user_agent" + }, + "screamingSnakeCase": { + "unsafeName": "USER_AGENT", + "safeName": "USER_AGENT" + }, + "pascalCase": { + "unsafeName": "UserAgent", + "safeName": "UserAgent" + } + } + }, + "typeReference": { + "type": "literal", + "value": { + "type": "string", + "value": "my-sdk" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "endpoints": { + "endpoint_.ping": { + "auth": null, + "declaration": { + "name": { + "originalName": "ping", + "camelCase": { + "unsafeName": "ping", + "safeName": "ping" + }, + "snakeCase": { + "unsafeName": "ping", + "safeName": "ping" + }, + "screamingSnakeCase": { + "unsafeName": "PING", + "safeName": "PING" + }, + "pascalCase": { + "unsafeName": "Ping", + "safeName": "Ping" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "location": { + "method": "GET", + "path": "/ping" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "casingsConfig": { + "generationLanguage": null, + "keywords": null, + "smartCasing": true + }, + "subpackages": {}, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": "service_", + "types": [], + "errors": [], + "subpackages": [], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": false, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": false, + "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/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts index 1c8a834c644e..5d4a742d7777 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts @@ -43,6 +43,7 @@ export async function createAndStartJob({ automationMode, autoMerge, skipIfNoDiff, + verify, loginCommand = "fern login" }: { projectConfig: fernConfigJson.ProjectConfig; @@ -68,6 +69,13 @@ export async function createAndStartJob({ automationMode?: boolean; autoMerge?: boolean; skipIfNoDiff?: boolean; + /** + * When true, Fiddle enables the generator-cli pipeline's VerificationStep, + * which runs `.fern/verify.sh` inside the language-specific validator + * container after the generator emits SDK files. Plumbed through from the + * CLI-level `--verify` flag. Default: false (verify off). + */ + verify?: boolean; /** * CLI command to reference in auth-failure hints (e.g. 'fern login' for v1, * 'fern auth login' for CLI v2). Defaults to 'fern login'. @@ -111,6 +119,7 @@ export async function createAndStartJob({ automationMode, autoMerge, skipIfNoDiff, + verify, loginCommand }), retryRateLimited, @@ -142,6 +151,7 @@ async function createJob({ pushPreviewBranch, fernignoreContents, skipIfNoDiff, + verify, loginCommand }: { projectConfig: fernConfigJson.ProjectConfig; @@ -163,6 +173,7 @@ async function createJob({ automationMode?: boolean; autoMerge?: boolean; skipIfNoDiff?: boolean; + verify?: boolean; loginCommand: string; }): Promise { const remoteGenerationService = createFiddleService({ token: token.value }); @@ -200,7 +211,8 @@ async function createJob({ preview: fiddlePreview ?? absolutePathToPreview != null, pushPreviewBranch, fernignoreContents, - skipIfNoDiff + skipIfNoDiff, + verify // TODO(FER-9671): Pass remaining automation flags to Fiddle once its API is updated: // automationMode, // autoMerge, diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForAPIWorkspace.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForAPIWorkspace.ts index a712d892ef98..ef87b1550ff0 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForAPIWorkspace.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForAPIWorkspace.ts @@ -47,6 +47,7 @@ export async function runRemoteGenerationForAPIWorkspace({ automationMode, autoMerge, skipIfNoDiff, + verify, noReplay, disableTelemetry, automation, @@ -85,6 +86,13 @@ export async function runRemoteGenerationForAPIWorkspace({ automationMode?: boolean; autoMerge?: boolean; skipIfNoDiff?: boolean; + /** + * `--verify` CLI flag. When true, Fiddle runs the generator-cli pipeline's + * VerificationStep against the language-specific validator after the generator + * emits SDK files. Forwarded per-generator to {@link runRemoteGenerationForGenerator}. + * Default: false. + */ + verify?: boolean; /** `--no-replay` CLI flag. Cloud doesn't honor it yet (FER-10343), plumbed for telemetry parity. */ noReplay?: boolean; /** Suppresses replay PostHog event when true. Honors FERN_DISABLE_TELEMETRY. */ @@ -156,6 +164,7 @@ export async function runRemoteGenerationForAPIWorkspace({ automationMode, autoMerge, skipIfNoDiff, + verify, noReplay, disableTelemetry, automation, @@ -212,6 +221,7 @@ async function generateOne({ automationMode, autoMerge, skipIfNoDiff, + verify, noReplay, disableTelemetry, automation, @@ -246,6 +256,7 @@ async function generateOne({ automationMode: boolean | undefined; autoMerge: boolean | undefined; skipIfNoDiff: boolean | undefined; + verify: boolean | undefined; noReplay: boolean | undefined; disableTelemetry: boolean | undefined; automation: AutomationRunOptions | undefined; @@ -332,6 +343,7 @@ async function generateOne({ automationMode, autoMerge, skipIfNoDiff, + verify, noReplay, disableTelemetry, loginCommand diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForGenerator.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForGenerator.ts index 8505c8ea72a3..3d3b77c21ae5 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForGenerator.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForGenerator.ts @@ -54,6 +54,7 @@ export async function runRemoteGenerationForGenerator({ automationMode, autoMerge, skipIfNoDiff, + verify, noReplay, disableTelemetry, loginCommand @@ -86,6 +87,12 @@ export async function runRemoteGenerationForGenerator({ automationMode?: boolean; autoMerge?: boolean; skipIfNoDiff?: boolean; + /** + * Whether the user passed `--verify`. When true, Fiddle runs the generator-cli + * pipeline's VerificationStep against the language-specific validator after the + * generator emits SDK files. Forwarded to {@link createAndStartJob}. Default: false. + */ + verify?: boolean; /** * Whether the user passed `--no-replay`. Currently a no-op on the cloud path * (Fiddle doesn't honor it yet — FER-10343 out-of-scope), but plumbed through @@ -332,6 +339,7 @@ export async function runRemoteGenerationForGenerator({ automationMode, autoMerge, skipIfNoDiff, + verify, loginCommand }); interactiveTaskContext.logger.debug(`Job ID: ${job.jobId}`); diff --git a/packages/cli/yaml/docs-validator/src/__test__/formatInitError.test.ts b/packages/cli/yaml/docs-validator/src/__test__/formatInitError.test.ts new file mode 100644 index 000000000000..3d42af1092ab --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/__test__/formatInitError.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { formatInitError } from "../formatInitError.js"; + +describe("formatInitError", () => { + it("returns the message of an Error instance", () => { + expect(formatInitError(new Error("boom"))).toBe("boom"); + }); + + it("returns string throws as-is", () => { + expect(formatInitError("kaboom")).toBe("kaboom"); + }); + + it("serializes plain objects to JSON instead of returning [object Object]", () => { + expect(formatInitError({ code: "FDR_TIMEOUT", attempt: 3 })).toBe('{"code":"FDR_TIMEOUT","attempt":3}'); + }); + + it("falls back to Object.prototype.toString for empty objects", () => { + // `{}` serializes to `"{}"` which is not informative; fall through to + // toString so users see at least the type tag. + expect(formatInitError({})).toBe("[object Object]"); + }); + + it("falls back to Object.prototype.toString when JSON.stringify throws", () => { + const circular: Record = {}; + circular.self = circular; + expect(formatInitError(circular)).toBe("[object Object]"); + }); + + it("does not crash on null or undefined", () => { + expect(formatInitError(null)).toBe("null"); + expect(formatInitError(undefined)).toBe("[object Undefined]"); + }); +}); diff --git a/packages/cli/yaml/docs-validator/src/formatInitError.ts b/packages/cli/yaml/docs-validator/src/formatInitError.ts new file mode 100644 index 000000000000..12856c6e3605 --- /dev/null +++ b/packages/cli/yaml/docs-validator/src/formatInitError.ts @@ -0,0 +1,22 @@ +/** + * Format an unknown thrown value (caught from a rule's `create()`) into a + * human-readable message. Avoids the unhelpful `[object Object]` that would + * otherwise come from `String({})`. + */ +export function formatInitError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + try { + const serialized = JSON.stringify(error); + if (serialized != null && serialized !== "{}") { + return serialized; + } + } catch { + // fall through to Object.prototype.toString + } + return Object.prototype.toString.call(error); +} diff --git a/packages/cli/yaml/docs-validator/src/rules/missing-redirects/missing-redirects.ts b/packages/cli/yaml/docs-validator/src/rules/missing-redirects/missing-redirects.ts index 043310947774..5d21f7b0b80f 100644 --- a/packages/cli/yaml/docs-validator/src/rules/missing-redirects/missing-redirects.ts +++ b/packages/cli/yaml/docs-validator/src/rules/missing-redirects/missing-redirects.ts @@ -1,10 +1,10 @@ import { getToken } from "@fern-api/auth"; -import { noop } from "@fern-api/core-utils"; import { DocsDefinitionResolver } from "@fern-api/docs-resolver"; import { FernNavigation } from "@fern-api/fdr-sdk"; -import { createLogger } from "@fern-api/logger"; -import { createMockTaskContext } from "@fern-api/task-context"; +import { createLogger, type LogLevel } from "@fern-api/logger"; +import { createMockTaskContext, type TaskContext } from "@fern-api/task-context"; +import { formatInitError } from "../../formatInitError.js"; import { Rule } from "../../Rule.js"; import { getInstanceUrls, toBaseUrl } from "../valid-markdown-link/url-utils.js"; import { buildPageIdToSlugMap } from "./buildPageIdToSlugMap.js"; @@ -15,7 +15,24 @@ import { type MarkdownEntry } from "./missing-redirects-logic.js"; -const NOOP_CONTEXT = createMockTaskContext({ logger: createLogger(noop) }); +/** + * Captures the last `error`-level message routed through the mock task context + * so we can surface it in the warning when `DocsDefinitionResolver.resolve()` + * aborts via `failAndThrow`. The mock's `failAndThrow` throws a + * `TaskAbortSignal` (a non-Error class) and logs the actual reason — without + * this capture the rule would only see `[object Object]`. + */ +function createResolverContext(): { context: TaskContext; getLastErrorMessage: () => string | undefined } { + let lastErrorMessage: string | undefined; + const context = createMockTaskContext({ + logger: createLogger((level: LogLevel, ...args: string[]) => { + if (level === "error") { + lastErrorMessage = args.join(" "); + } + }) + }); + return { context, getLastErrorMessage: () => lastErrorMessage }; +} // The FDR SDK types config.root as {} via zod inference, but at runtime it is FernNavigation.V1.RootNode. // This type guard checks the "type" discriminant to safely narrow the type without a blind cast. @@ -28,6 +45,7 @@ type FetchResult = | { type: "no-token" } | { type: "no-instance-url" } | { type: "fetch-failed"; reason: string } + | { type: "resolve-failed"; reason: string } | { type: "first-publish" }; /** @@ -103,29 +121,41 @@ export const MissingRedirectsRule: Rule = { return makeSkipVisitor({ type: "first-publish" }); } - const docsDefinitionResolver = new DocsDefinitionResolver({ - domain: url, - docsWorkspace: workspace, - ossWorkspaces, - apiWorkspaces, - taskContext: NOOP_CONTEXT, - editThisPage: undefined, - uploadFiles: undefined, - registerApi: undefined, - targetAudiences: undefined - }); + let removedSlugs: ReturnType; + const { context: resolverContext, getLastErrorMessage } = createResolverContext(); + try { + const docsDefinitionResolver = new DocsDefinitionResolver({ + domain: url, + docsWorkspace: workspace, + ossWorkspaces, + apiWorkspaces, + taskContext: resolverContext, + editThisPage: undefined, + uploadFiles: undefined, + registerApi: undefined, + targetAudiences: undefined + }); + + const resolvedDocsDefinition = await docsDefinitionResolver.resolve(); + const configRoot = resolvedDocsDefinition.config.root; + if (!configRoot || !isV1RootNode(configRoot)) { + return {}; + } - const resolvedDocsDefinition = await docsDefinitionResolver.resolve(); - const configRoot = resolvedDocsDefinition.config.root; - if (!configRoot || !isV1RootNode(configRoot)) { - return {}; + const root = FernNavigation.migrate.FernNavigationV1ToLatest.create().root(configRoot); + const localPageIdToSlug = buildPageIdToSlugMap(root); + const latestEntries = keepLatestEntryPerPageId(result.entries); + removedSlugs = findRemovedSlugs(latestEntries, localPageIdToSlug); + } catch (error) { + // `DocsDefinitionResolver` reports fatal errors via `taskContext.failAndThrow`, + // which throws a `TaskAbortSignal` (a non-Error sentinel class) after logging + // the real reason. Prefer the captured log message over `String(error)` so the + // warning includes something actionable instead of `[object Object]`. + const reason = getLastErrorMessage() ?? formatInitError(error); + logger.debug(`[missing-redirects] Failed to resolve local docs navigation: ${reason}`); + return makeSkipVisitor({ type: "resolve-failed", reason }); } - const root = FernNavigation.migrate.FernNavigationV1ToLatest.create().root(configRoot); - const localPageIdToSlug = buildPageIdToSlugMap(root); - const latestEntries = keepLatestEntryPerPageId(result.entries); - const removedSlugs = findRemovedSlugs(latestEntries, localPageIdToSlug); - if (removedSlugs.length === 0) { return {}; } @@ -178,5 +208,16 @@ function makeSkipVisitor(fetchResult: Exclude) } ] }; + case "resolve-failed": + return { + file: () => [ + { + severity: "warning", + message: + `Missing redirects check skipped: failed to resolve local docs navigation (${fetchResult.reason}). ` + + "Run `fern check --log-level=debug` for more detail." + } + ] + }; } } diff --git a/packages/cli/yaml/docs-validator/src/validateDocsWorkspace.ts b/packages/cli/yaml/docs-validator/src/validateDocsWorkspace.ts index c00fb5d84ed9..7f49ec0649b4 100644 --- a/packages/cli/yaml/docs-validator/src/validateDocsWorkspace.ts +++ b/packages/cli/yaml/docs-validator/src/validateDocsWorkspace.ts @@ -10,6 +10,7 @@ import { type SeverityOverride } from "./createDocsConfigFileAstVisitorForRules.js"; import { visitDocsConfigFileYamlAst } from "./docsAst/visitDocsConfigFileYamlAst.js"; +import { formatInitError } from "./formatInitError.js"; import { getAllRules } from "./getAllRules.js"; import { Rule } from "./Rule.js"; import { MissingRedirectsRule } from "./rules/missing-redirects/index.js"; @@ -127,17 +128,24 @@ export async function runRulesOnDocsWorkspace({ const allRulesWithVisitors: RuleWithVisitor[] = []; for (const result of ruleCreationResults) { if ("error" in result) { - const message = result.error instanceof Error ? result.error.message : String(result.error); + const message = formatInitError(result.error); + const severityOverride = severityOverrides.get(result.ruleName); + // Honor the user's configured severity for init failures. When a + // rule is configured at `warn` we should surface the failure as a + // warning rather than a fatal — otherwise the override is + // silently bypassed whenever the rule throws during setup. + const severity: ValidationViolation["severity"] = + severityOverride === "warning" ? "warning" : severityOverride === "error" ? "error" : "fatal"; violations.push({ name: result.ruleName, - severity: "fatal", + severity, relativeFilepath: RelativeFilePath.of(DOCS_CONFIGURATION_FILENAME), nodePath: [], message: `Rule "${result.ruleName}" failed to initialize: ${message}` }); context.logger.debug( `Rule "${result.ruleName}" failed to initialize: ${ - result.error instanceof Error ? (result.error.stack ?? result.error.message) : String(result.error) + result.error instanceof Error ? (result.error.stack ?? result.error.message) : message }` ); } else { diff --git a/packages/generator-cli/src/__test__/pushSignedCommit.test.ts b/packages/generator-cli/src/__test__/pushSignedCommit.test.ts index 4f6353594e16..99c37af07fc9 100644 --- a/packages/generator-cli/src/__test__/pushSignedCommit.test.ts +++ b/packages/generator-cli/src/__test__/pushSignedCommit.test.ts @@ -102,7 +102,9 @@ describe("pushSignedCommit", () => { repo: "acme-sdk", message: "SDK Generation", tree: "tree-sha", - parents: ["parent-sha"] + parents: ["parent-sha"], + author: { name: "fern-api", email: "115122769+fern-api[bot]@users.noreply.github.com" }, + committer: { name: "fern-api", email: "115122769+fern-api[bot]@users.noreply.github.com" } }); expect(octokit.git.updateRef).toHaveBeenCalledWith({ owner: "acme", @@ -226,7 +228,9 @@ describe("pushSignedCommit", () => { repo: "acme-sdk", message: "SDK Generation", tree: "tree-sha-2", - parents: ["parent-sha-2"] + parents: ["parent-sha-2"], + author: { name: "fern-api", email: "115122769+fern-api[bot]@users.noreply.github.com" }, + committer: { name: "fern-api", email: "115122769+fern-api[bot]@users.noreply.github.com" } }); // Temp ref re-push on the rebase retry must force, because the rebased commit // is not a descendant of the original tempRef tip. diff --git a/packages/generator-cli/src/pipeline/github/pushSignedCommit.ts b/packages/generator-cli/src/pipeline/github/pushSignedCommit.ts index 5e3dd531b132..7fe7fa2104da 100644 --- a/packages/generator-cli/src/pipeline/github/pushSignedCommit.ts +++ b/packages/generator-cli/src/pipeline/github/pushSignedCommit.ts @@ -3,9 +3,15 @@ import type { ClonedRepository } from "@fern-api/github"; import type { Octokit } from "@octokit/rest"; import type { PipelineLogger } from "../PipelineLogger"; +import { FERN_BOT_EMAIL, FERN_BOT_NAME } from "./constants"; const MAX_CONCURRENT_PUSH_RETRIES = 3; +export interface CommitAuthor { + name: string; + email: string; +} + export interface PushSignedCommitOptions { repository: ClonedRepository; octokit: Octokit; @@ -26,6 +32,11 @@ export interface PushSignedCommitOptions { * remote changes) is only safe for bot-owned branches and is therefore never done here. */ rebaseOnConflict?: boolean; + /** + * Override the commit author and committer identity. + * Defaults to the Fern bot identity (fern-api / fern-api[bot] noreply email). + */ + author?: CommitAuthor; logger: PipelineLogger; } @@ -48,6 +59,7 @@ export async function pushSignedCommit({ branch, force, rebaseOnConflict = false, + author, logger }: PushSignedCommitOptions): Promise { const tempRef = `refs/temp/fern-${Date.now()}`; @@ -65,12 +77,18 @@ export async function pushSignedCommit({ tempRefPushed = true; for (let attempt = 0; attempt < MAX_CONCURRENT_PUSH_RETRIES; attempt++) { + const commitAuthor = { + name: author?.name ?? FERN_BOT_NAME, + email: author?.email ?? FERN_BOT_EMAIL + }; const { data: signedCommit } = await octokit.git.createCommit({ owner, repo, message, tree: treeSha, - parents + parents, + author: commitAuthor, + committer: commitAuthor }); const signedSha = signedCommit.sha; diff --git a/packages/generator-cli/src/pipeline/steps/GithubStep.ts b/packages/generator-cli/src/pipeline/steps/GithubStep.ts index fd09a3963828..6714048d0ed9 100644 --- a/packages/generator-cli/src/pipeline/steps/GithubStep.ts +++ b/packages/generator-cli/src/pipeline/steps/GithubStep.ts @@ -197,6 +197,7 @@ export class GithubStep extends BaseStep { repo, branch: prBranch, force: isUpdatingExistingPR, + author: this.config.author, logger: this.logger }); const pushedBranch = await repository.getCurrentBranch(); @@ -342,6 +343,7 @@ export class GithubStep extends BaseStep { branch: baseBranch, force: false, rebaseOnConflict: true, + author: this.config.author, logger: this.logger }); diff --git a/packages/generator-cli/src/pipeline/types.ts b/packages/generator-cli/src/pipeline/types.ts index a67319db9c52..1221764af380 100644 --- a/packages/generator-cli/src/pipeline/types.ts +++ b/packages/generator-cli/src/pipeline/types.ts @@ -152,6 +152,11 @@ export interface GithubStepConfig { runId?: string; /** GitHub API base URL for GitHub Enterprise (e.g. "https://github.intuit.com/api/v3"). Omit for github.com. */ apiBaseUrl?: string; + /** Override the commit author/committer identity for API-created commits. Defaults to the Fern bot identity. */ + author?: { + name: string; + email: string; + }; } export interface PipelineResult { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7a78af69fc0..39e9d06ee030 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ catalogs: specifier: 0.0.6-2ee1b7e28 version: 0.0.6-2ee1b7e28 '@fern-api/generator-cli': - specifier: 0.9.22 - version: 0.9.22 + specifier: 0.9.27 + version: 0.9.27 '@fern-api/venus-api-sdk': specifier: 0.22.34 version: 0.22.34 @@ -49,8 +49,8 @@ catalogs: specifier: ^0.0.5297 version: 0.0.5297 '@fern-fern/fiddle-sdk': - specifier: 1.0.3 - version: 1.0.3 + specifier: 1.1.0 + version: 1.1.0 '@fern-fern/generator-exec-sdk': specifier: ^0.0.1167 version: 0.0.1167 @@ -630,7 +630,7 @@ importers: version: link:packages/configs '@fern-api/generator-cli': specifier: 'catalog:' - version: 0.9.22 + version: 0.9.27 '@rolldown/binding-darwin-arm64': specifier: 'catalog:' version: 1.0.0 @@ -699,7 +699,7 @@ importers: version: link:../../packages/commons/fs-utils '@fern-api/generator-cli': specifier: 'catalog:' - version: 0.9.22 + version: 0.9.27 '@fern-api/ir-sdk': specifier: workspace:* version: link:../../packages/ir-sdk @@ -2466,7 +2466,7 @@ importers: version: link:../../../packages/commons/fs-utils '@fern-api/generator-cli': specifier: 'catalog:' - version: 0.9.22 + version: 0.9.27 '@fern-api/logger': specifier: workspace:* version: link:../../../packages/cli/logger @@ -4473,7 +4473,7 @@ importers: version: link:../workspace/loader '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@fern-fern/generators-sdk': specifier: 'catalog:' version: 0.114.0-5745f9e74 @@ -4886,7 +4886,7 @@ importers: version: link:../yaml/loader '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@fern-fern/generators-sdk': specifier: 'catalog:' version: 0.114.0-5745f9e74 @@ -5022,7 +5022,7 @@ importers: version: link:../../commons/path-utils '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -5065,7 +5065,7 @@ importers: version: link:../task-context '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@fern-fern/generators-sdk': specifier: 'catalog:' version: 0.114.0-5745f9e74 @@ -5353,6 +5353,9 @@ importers: packages/cli/docs-preview: dependencies: + '@fern-api/auth': + specifier: workspace:* + version: link:../auth '@fern-api/core-utils': specifier: workspace:* version: link:../../commons/core-utils @@ -6157,7 +6160,7 @@ importers: version: link:../../../workspace/loader '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@fern-fern/generator-exec-sdk': specifier: 'catalog:' version: 0.0.1167 @@ -6317,7 +6320,7 @@ importers: version: link:../../../workspace/loader '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 axios: specifier: 'catalog:' version: 1.16.0 @@ -7054,7 +7057,7 @@ importers: version: link:../../api-importers/v3-importer-commons '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@open-rpc/meta-schema': specifier: 'catalog:' version: 1.14.9 @@ -7495,7 +7498,7 @@ importers: version: link:../../cli/task-context '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@redocly/openapi-core': specifier: 'catalog:' version: 1.34.11 @@ -7897,7 +7900,7 @@ importers: version: 0.0.5297 '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@fern-fern/generators-sdk': specifier: 'catalog:' version: 0.114.0-5745f9e74 @@ -8147,7 +8150,7 @@ importers: version: link:../cli/workspace/loader '@fern-fern/fiddle-sdk': specifier: 'catalog:' - version: 1.0.3 + version: 1.1.0 '@fern-fern/generators-sdk': specifier: 'catalog:' version: 0.114.0-5745f9e74 @@ -9447,13 +9450,8 @@ packages: '@fern-api/fdr-sdk@1.2.4-f661387fb2': resolution: {integrity: sha512-wlk1lTCIZ7biND4vQf8jvhUw9P/rBQ5pXASCrumv8R96up0B3DY6yiY1C4VmFyHmp/kPhcjzc5T9TvHZZxFdrA==} - '@fern-api/generator-cli@0.9.22': - resolution: {integrity: sha512-War2x7+HMAl8TzferxbIRfp1zgQ5OMdoZW/FsD2H/Ep7t0SWLBDcAm+x7s+mhnwEYYXe9VSdA2+aNZC3pwRalA==} - hasBin: true - - '@fern-api/replay@0.14.1': - resolution: {integrity: sha512-OBuBJb7HWMkWjhKpd91NW5pfPtpzvAEvkkFPVtN70K9uLPoc8CQm0j/asaEaOjH/tm7cXDAEQZj4Ekn1Rj+XNw==} - engines: {node: '>=18'} + '@fern-api/generator-cli@0.9.27': + resolution: {integrity: sha512-01GBGwtXm5gZ06amH/V8Bb7sGSWiSBMQnG6hUyTbJK2qaXNUHygNGqOjOIF5XDwjXww6DZLLsJfEoEAzaUFflg==} hasBin: true '@fern-api/replay@0.15.0': @@ -9474,8 +9472,8 @@ packages: '@fern-fern/fdr-test-sdk@0.0.5297': resolution: {integrity: sha512-jrZUZ6oIA64LHtrv77xEq0X7qJhT9xRMGWhKcLjIUArMsD7h6KMWUHVdoB/1MP0Mz/uL/E2xmM31SOgJicpIcA==} - '@fern-fern/fiddle-sdk@1.0.3': - resolution: {integrity: sha512-JXR2YRzitbGdd1cioDkUhEQNuq1SckJJpCahJKHTPROQirRnjVHJrzC6PG2MZpuCbgSw6cg4TmqRVm66VISBkg==} + '@fern-fern/fiddle-sdk@1.1.0': + resolution: {integrity: sha512-NVAyCRgsLr3SdX5XsOKhQRpnCr7OyYw7pQozEw0HH8/szD9JVqrg0CaK6GX0cwmHy1JhMQvdsiiNOVZ56Xb/3w==} engines: {node: '>=18.0.0'} '@fern-fern/generator-cli-sdk@0.1.8': @@ -16385,10 +16383,10 @@ snapshots: - encoding - typescript - '@fern-api/generator-cli@0.9.22': + '@fern-api/generator-cli@0.9.27': dependencies: '@boundaryml/baml': 0.219.0 - '@fern-api/replay': 0.14.1 + '@fern-api/replay': 0.15.0 '@octokit/rest': 22.0.1 es-toolkit: 1.45.1 semver: 7.7.4 @@ -16396,15 +16394,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@fern-api/replay@0.14.1': - dependencies: - minimatch: 10.2.5 - node-diff3: 3.2.0 - simple-git: 3.36.0 - yaml: 2.8.3 - transitivePeerDependencies: - - supports-color - '@fern-api/replay@0.15.0': dependencies: minimatch: 10.2.5 @@ -16436,7 +16425,7 @@ snapshots: transitivePeerDependencies: - encoding - '@fern-fern/fiddle-sdk@1.0.3': {} + '@fern-fern/fiddle-sdk@1.1.0': {} '@fern-fern/generator-cli-sdk@0.1.8': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 02ec2f3df6e5..99e7f58d0fb5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -68,12 +68,12 @@ catalog: "@bufbuild/protoplugin": 2.2.5 "@fern-api/fai-sdk": 0.0.6-2ee1b7e28 "@fern-api/fdr-sdk": 1.2.4-f661387fb2 - "@fern-api/generator-cli": 0.9.22 + "@fern-api/generator-cli": 0.9.27 "@fern-api/ui-core-utils": 0.129.4-b6c699ad2 "@fern-api/venus-api-sdk": 0.22.34 "@fern-fern/docs-config": 0.0.80 "@fern-fern/fdr-test-sdk": ^0.0.5297 - "@fern-fern/fiddle-sdk": 1.0.3 + "@fern-fern/fiddle-sdk": 1.1.0 "@fern-fern/generator-exec-sdk": ^0.0.1167 "@fern-fern/generators-sdk": 0.114.0-5745f9e74 "@fern-fern/ir-v53-sdk": 1.0.1 diff --git a/scripts/copy-openapi-specs.sh b/scripts/copy-openapi-specs.sh new file mode 100755 index 000000000000..f18b66248dc7 --- /dev/null +++ b/scripts/copy-openapi-specs.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -euo pipefail + +# Copy seed-generated OpenAPI specs into test-definition directories and +# generate x-fern-sdk-group-name / x-fern-sdk-method-name overrides. +# Does NOT modify generators.yml — use wire-openapi-specs.sh for that. +# +# Usage: +# scripts/copy-openapi-specs.sh # all fixtures +# scripts/copy-openapi-specs.sh imdb exhaustive # specific fixtures + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SEED_OPENAPI="$REPO_ROOT/seed/openapi" +TEST_DEFS="$REPO_ROOT/test-definitions/fern/apis" + +copied=0 +skipped=0 +no_seed_spec=0 + +if [ $# -gt 0 ]; then + fixtures=("$@") +else + fixtures=() + for dir in "$TEST_DEFS"/*/; do + fixtures+=("$(basename "$dir")") + done +fi + +for api_name in "${fixtures[@]}"; do + dir="$TEST_DEFS/$api_name" + if [ ! -d "$dir" ]; then + echo "✗ $api_name (no test definition directory)" + continue + fi + + gen_file="$dir/generators.yml" + + # Skip fixtures already wired to a non-definition input + if grep -q '^api:' "$gen_file" 2>/dev/null; then + skipped=$((skipped + 1)) + continue + fi + + seed_spec="" + if [ -f "$SEED_OPENAPI/$api_name/openapi.yml" ]; then + seed_spec="$SEED_OPENAPI/$api_name/openapi.yml" + elif [ -f "$SEED_OPENAPI/$api_name/no-custom-config/openapi.yml" ]; then + seed_spec="$SEED_OPENAPI/$api_name/no-custom-config/openapi.yml" + fi + + if [ -z "$seed_spec" ]; then + echo "✗ $api_name (no seed openapi spec)" + no_seed_spec=$((no_seed_spec + 1)) + continue + fi + + cp "$seed_spec" "$dir/openapi.yml" + + # Generate overrides if a definition/ directory exists (needed for group/method naming) + if [ -d "$dir/definition" ]; then + python3 "$SCRIPT_DIR/generate-openapi-overrides.py" "$dir/openapi.yml" "$dir/openapi-overrides.yml" + fi + + echo "✓ $api_name" + copied=$((copied + 1)) +done + +echo "" +echo "================================" +echo "Copy OpenAPI Specs Results" +echo "================================" +echo "Copied: $copied" +echo "Skipped: $skipped (already wired to spec)" +echo "No seed spec: $no_seed_spec" +echo "================================" diff --git a/scripts/export-openapi-test.sh b/scripts/export-openapi-test.sh index aea1c604697e..cac7955c1872 100755 --- a/scripts/export-openapi-test.sh +++ b/scripts/export-openapi-test.sh @@ -3,50 +3,88 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SEED_OPENAPI="$REPO_ROOT/seed/openapi" MAX_PARALLEL="${MAX_PARALLEL:-10}" RESULTS_DIR="$REPO_ROOT/.local/tmp" mkdir -p "$RESULTS_DIR" rm -f "$RESULTS_DIR"/*.result "$RESULTS_DIR"/*.stdout -# Collect API directories, skipping those that are already OpenAPI specs -cd "$REPO_ROOT/test-definitions" -CLI_PATH="$REPO_ROOT/packages/cli/cli/dist/prod/cli.cjs" +cd "$REPO_ROOT" + +# --------------------------------------------------------------------------- +# Step 1: For each test-definition API, find its seed openapi spec +# --------------------------------------------------------------------------- apis=() -skipped=0 -for dir in fern/apis/*/; do +no_seed_spec=0 +no_seed_spec_apis=() + +for dir in test-definitions/fern/apis/*/; do api_name=$(basename "$dir") - # Skip APIs that use an OpenAPI spec (have "api: specs:" in generators.yml) - if grep -q "specs:" "$dir/generators.yml" 2>/dev/null; then - ((skipped++)) + + seed_spec="" + if [ -f "$SEED_OPENAPI/$api_name/openapi.yml" ]; then + seed_spec="$SEED_OPENAPI/$api_name/openapi.yml" + elif [ -f "$SEED_OPENAPI/$api_name/no-custom-config/openapi.yml" ]; then + seed_spec="$SEED_OPENAPI/$api_name/no-custom-config/openapi.yml" + fi + + if [ -z "$seed_spec" ]; then + no_seed_spec=$((no_seed_spec + 1)) + no_seed_spec_apis+=("$api_name") continue fi + apis+=("$api_name") done total=${#apis[@]} -echo "Found $total Fern definition APIs to export (skipped $skipped OpenAPI specs)" +echo "Found $total APIs with seed openapi specs ($no_seed_spec without)" + +# --------------------------------------------------------------------------- +# Step 2: Validate seed specs with Spectral +# --------------------------------------------------------------------------- +echo "" +echo "Validating specs with Spectral..." + +find_seed_spec() { + local api_name="$1" + if [ -f "$SEED_OPENAPI/$api_name/openapi.yml" ]; then + echo "$SEED_OPENAPI/$api_name/openapi.yml" + elif [ -f "$SEED_OPENAPI/$api_name/no-custom-config/openapi.yml" ]; then + echo "$SEED_OPENAPI/$api_name/no-custom-config/openapi.yml" + fi +} -# Run exports in parallel -run_export() { +run_spectral() { local api_name="$1" local results_dir="$2" local output_file="$results_dir/$api_name" + local seed_spec + seed_spec=$(find_seed_spec "$api_name") - if FERN_NO_VERSION_REDIRECTION=true node "$CLI_PATH" export "fern/apis/$api_name/openapi.yml" --api "$api_name" > "$output_file.stdout" 2>&1; then + if npx --yes @stoplight/spectral-cli lint "$seed_spec" > "$output_file.stdout" 2>&1; then echo "pass" > "$output_file.result" echo "✓ $api_name" else - echo "fail" > "$output_file.result" - echo "✗ $api_name" + # Spectral exits non-zero for warnings too; check if there are actual errors + if grep -q "error" "$output_file.stdout" 2>/dev/null; then + echo "fail" > "$output_file.result" + echo "✗ $api_name" + else + echo "pass" > "$output_file.result" + echo "✓ $api_name (warnings only)" + fi fi } -export -f run_export -export CLI_PATH RESULTS_DIR +export -f run_spectral find_seed_spec +export SEED_OPENAPI RESULTS_DIR -printf '%s\n' "${apis[@]}" | xargs -P "$MAX_PARALLEL" -I {} bash -c 'run_export "$@"' _ {} "$RESULTS_DIR" +printf '%s\n' "${apis[@]}" | xargs -P "$MAX_PARALLEL" -I {} bash -c 'run_spectral "$@"' _ {} "$RESULTS_DIR" -# Tally results +# --------------------------------------------------------------------------- +# Step 3: Tally results +# --------------------------------------------------------------------------- passed=0 failed=0 failed_apis=() @@ -54,33 +92,42 @@ failed_apis=() for api_name in "${apis[@]}"; do result=$(cat "$RESULTS_DIR/$api_name.result" 2>/dev/null || echo "fail") if [ "$result" = "pass" ]; then - ((passed++)) + passed=$((passed + 1)) else - ((failed++)) + failed=$((failed + 1)) failed_apis+=("$api_name") fi done -# Print summary +# --------------------------------------------------------------------------- +# Step 4: Print summary +# --------------------------------------------------------------------------- echo "" echo "================================" -echo "OpenAPI Export Results" +echo "OpenAPI Spec Validation Results" echo "================================" -echo "Total: $total" -echo "Passed: $passed" -echo "Failed: $failed" -echo "Skipped: $skipped (already OpenAPI)" +echo "Total: $total" +echo "Passed: $passed" +echo "Failed: $failed" +echo "No seed spec: $no_seed_spec" echo "================================" +if [ ${#no_seed_spec_apis[@]} -gt 0 ]; then + echo "" + echo "APIs missing seed openapi spec:" + for api in "${no_seed_spec_apis[@]}"; do + echo " - $api" + done +fi + if [ ${#failed_apis[@]} -gt 0 ]; then echo "" - echo "Failed APIs:" + echo "APIs with Spectral errors:" for api in "${failed_apis[@]}"; do echo " - $api" - # Print last 5 lines of output for failed APIs if [ -f "$RESULTS_DIR/$api.stdout" ]; then - echo " Output (last 5 lines):" - tail -5 "$RESULTS_DIR/$api.stdout" | sed 's/^/ /' + echo " Output (last 10 lines):" + tail -10 "$RESULTS_DIR/$api.stdout" | sed 's/^/ /' fi done fi @@ -90,22 +137,22 @@ if [ -n "${GITHUB_OUTPUT:-}" ]; then echo "total=$total" >> "$GITHUB_OUTPUT" echo "passed=$passed" >> "$GITHUB_OUTPUT" echo "failed=$failed" >> "$GITHUB_OUTPUT" - echo "skipped=$skipped" >> "$GITHUB_OUTPUT" + echo "no_seed_spec=$no_seed_spec" >> "$GITHUB_OUTPUT" fi # GitHub Actions step summary if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { - echo "## OpenAPI Export Results" + echo "## OpenAPI Spec Validation Results" echo "| Metric | Count |" echo "|--------|-------|" echo "| Total | $total |" echo "| Passed | $passed |" echo "| Failed | $failed |" - echo "| Skipped (already OpenAPI) | $skipped |" + echo "| No seed spec | $no_seed_spec |" if [ ${#failed_apis[@]} -gt 0 ]; then echo "" - echo "### Failed APIs" + echo "### APIs with Spectral Errors" for api in "${failed_apis[@]}"; do echo "- \`$api\`" done @@ -113,7 +160,7 @@ if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then } >> "$GITHUB_STEP_SUMMARY" fi -# Exit with failure if any APIs failed +# Exit with failure if any APIs failed validation if [ "$failed" -gt 0 ]; then exit 1 fi diff --git a/scripts/generate-openapi-overrides.py b/scripts/generate-openapi-overrides.py new file mode 100755 index 000000000000..84060659ae88 --- /dev/null +++ b/scripts/generate-openapi-overrides.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Generate x-fern-sdk-group-name and x-fern-sdk-method-name overrides +from a seed OpenAPI spec, using the Fern definition directory structure +to determine the nested package hierarchy.""" + +import sys +import os +import yaml + + +def to_camel(name: str) -> str: + """Convert kebab-case to camelCase: 'content-type' -> 'contentType'.""" + parts = name.split("-") + return parts[0] + "".join(p.capitalize() for p in parts[1:]) + + +def build_package_map(definition_dir: str) -> dict[str, list[str]]: + """Build a map from CamelCase tag to nested group path. + + Scans the Fern definition directory: + definition/endpoints/container.yml -> EndpointsContainer -> [endpoints, container] + definition/inlined-requests.yml -> InlinedRequests -> [inlinedRequests] + """ + pkg_map: dict[str, list[str]] = {} + + def scan_dir(dir_path: str, prefix_parts: list[str]): + try: + entries = sorted(os.listdir(dir_path)) + except OSError: + return + for entry in entries: + full = os.path.join(dir_path, entry) + if os.path.isdir(full): + scan_dir(full, prefix_parts + [to_camel(entry)]) + elif entry.endswith(".yml") and entry != "api.yml": + name = entry[:-4] + parts = prefix_parts + [to_camel(name)] + # Tag is PascalCase join of all parts + tag = "".join(p[0].upper() + p[1:] for p in parts) + pkg_map[tag] = parts + + if os.path.isdir(definition_dir): + scan_dir(definition_dir, []) + + return pkg_map + + +def generate_overrides(openapi_path: str) -> dict: + with open(openapi_path) as f: + spec = yaml.safe_load(f) + + api_dir = os.path.dirname(openapi_path) + definition_dir = os.path.join(api_dir, "definition") + pkg_map = build_package_map(definition_dir) + + overrides: dict = {"paths": {}} + + for path, methods in (spec.get("paths") or {}).items(): + for method, operation in methods.items(): + if not isinstance(operation, dict): + continue + + op_id = operation.get("operationId", "") + tags = operation.get("tags", []) + if not op_id or not tags: + continue + + tag = tags[0] + + # Determine group from definition structure, fall back to lowercased tag + if tag in pkg_map: + group = pkg_map[tag] + else: + group = [tag[0].lower() + tag[1:]] + + # Extract method name by stripping the underscore-joined group prefix + # operationId format: group1_group2_methodName + group_prefix = "_".join(group) + "_" + if op_id.startswith(group_prefix): + method_name = op_id[len(group_prefix):] + else: + method_name = op_id + + if path not in overrides["paths"]: + overrides["paths"][path] = {} + + overrides["paths"][path][method] = { + "x-fern-sdk-group-name": group, + "x-fern-sdk-method-name": method_name, + } + + return overrides + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [output.yml]", file=sys.stderr) + sys.exit(1) + + openapi_path = sys.argv[1] + output_path = sys.argv[2] if len(sys.argv) > 2 else None + + overrides = generate_overrides(openapi_path) + + # Avoid YAML anchors/aliases for cleaner output + noalias = yaml.dumper.Dumper + noalias.ignore_aliases = lambda self, data: True + output = yaml.dump(overrides, default_flow_style=False, sort_keys=False, Dumper=noalias) + + if output_path: + with open(output_path, "w") as f: + f.write(output) + print(f"✓ Wrote overrides to {output_path}") + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/wire-openapi-specs.sh b/scripts/wire-openapi-specs.sh new file mode 100755 index 000000000000..5e00421a5c0b --- /dev/null +++ b/scripts/wire-openapi-specs.sh @@ -0,0 +1,87 @@ +#!/bin/bash +set -euo pipefail + +# Wire test-definition fixtures to use their openapi.yml via api.specs in +# generators.yml. Only touches fixtures that currently use a Fern definition +# as input (no api: block). Skips fixtures already wired to any spec type +# (openapi, proto, asyncapi, etc.). +# +# Prereq: run copy-openapi-specs.sh first to ensure openapi.yml exists. +# +# Usage: +# scripts/wire-openapi-specs.sh # all eligible fixtures +# scripts/wire-openapi-specs.sh imdb exhaustive # specific fixtures + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +TEST_DEFS="$REPO_ROOT/test-definitions/fern/apis" + +wired=0 +skipped_already_wired=0 +skipped_no_spec=0 + +if [ $# -gt 0 ]; then + fixtures=("$@") +else + fixtures=() + for dir in "$TEST_DEFS"/*/; do + fixtures+=("$(basename "$dir")") + done +fi + +for api_name in "${fixtures[@]}"; do + dir="$TEST_DEFS/$api_name" + gen_file="$dir/generators.yml" + + if [ ! -d "$dir" ]; then + echo "✗ $api_name (no test definition directory)" + continue + fi + + # Skip fixtures that already have an api: block (already wired to some spec) + if grep -q '^api:' "$gen_file" 2>/dev/null; then + skipped_already_wired=$((skipped_already_wired + 1)) + continue + fi + + # Must have an openapi.yml to wire + if [ ! -f "$dir/openapi.yml" ]; then + echo "✗ $api_name (no openapi.yml — run copy-openapi-specs.sh first)" + skipped_no_spec=$((skipped_no_spec + 1)) + continue + fi + + if [ -f "$dir/openapi-overrides.yml" ]; then + api_block="api:\n specs:\n - openapi: ./openapi.yml\n overrides: ./openapi-overrides.yml" + else + api_block="api:\n specs:\n - openapi: ./openapi.yml" + fi + + if [ ! -f "$gen_file" ]; then + printf '# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json\n%b\n' "$api_block" > "$gen_file" + else + tmp="${TMPDIR:-/tmp}/wire-openapi-specs-$$" + first_line=$(head -1 "$gen_file") + if echo "$first_line" | grep -q "^# yaml-language-server"; then + echo "$first_line" > "$tmp" + printf '%b\n' "$api_block" >> "$tmp" + tail -n +2 "$gen_file" | grep -v "^{}$" >> "$tmp" || true + else + printf '%b\n' "$api_block" > "$tmp" + grep -v "^{}$" "$gen_file" >> "$tmp" || true + fi + mv "$tmp" "$gen_file" + fi + + echo "✓ $api_name" + wired=$((wired + 1)) +done + +echo "" +echo "================================" +echo "Wire OpenAPI Specs Results" +echo "================================" +echo "Wired: $wired" +echo "Already wired: $skipped_already_wired (have api: block)" +echo "No openapi.yml: $skipped_no_spec" +echo "================================" diff --git a/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example1.cs b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example1.cs index 61937b64dc8a..4498ddef78a5 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example2.cs b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example2.cs index 72625d8b76c6..e6f3f8dea28f 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example3.cs b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example3.cs new file mode 100644 index 000000000000..1924e97abca3 --- /dev/null +++ b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example4.cs b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example4.cs new file mode 100644 index 000000000000..2f14481a2f53 --- /dev/null +++ b/seed/csharp-sdk/imdb/exception-class-names/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/exception-class-names/reference.md b/seed/csharp-sdk/imdb/exception-class-names/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/reference.md +++ b/seed/csharp-sdk/imdb/exception-class-names/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat -
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/exception-class-names/snippet.json b/seed/csharp-sdk/imdb/exception-class-names/snippet.json index 1d9ade8c9658..9f0ab8270c8a 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/snippet.json +++ b/seed/csharp-sdk/imdb/exception-class-names/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index 50e1734c57c3..df598a666c77 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index 6bbf5029779f..8ab69c940103 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Exceptions/NotFoundError.cs similarity index 71% rename from seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs rename to seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Exceptions/NotFoundError.cs index 26cc08252dd2..307a15796591 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Exceptions/NotFoundError.cs @@ -4,8 +4,7 @@ namespace SeedApi; /// This exception type will be thrown for any non-2XX API responses. /// [Serializable] -public class MovieDoesNotExistError(string body) - : CustomApiException("MovieDoesNotExistError", 404, body) +public class NotFoundError(string body) : CustomApiException("NotFoundError", 404, body) { /// /// The body of the response that triggered the exception. diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/ImdbClient.cs index f2c02b603f60..42726f5cf911 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/ImdbClient.cs @@ -29,9 +29,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -79,7 +80,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -96,8 +97,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -143,9 +144,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( - JsonUtils.Deserialize(responseBody) - ); + throw new NotFoundError(JsonUtils.Deserialize(responseBody)); } } catch (JsonException) @@ -178,16 +177,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/exception-class-names/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Types/Movie.cs diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example1.cs b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example1.cs index e17572528a2d..5fb1f0cde298 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example2.cs b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example2.cs index 61970cd30083..1fe300e3253a 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example3.cs b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example3.cs new file mode 100644 index 000000000000..8a59f56c7db9 --- /dev/null +++ b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new CustomClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example4.cs b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example4.cs new file mode 100644 index 000000000000..a0007d0e48ed --- /dev/null +++ b/seed/csharp-sdk/imdb/exported-client-class-name/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new CustomClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/reference.md b/seed/csharp-sdk/imdb/exported-client-class-name/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/reference.md +++ b/seed/csharp-sdk/imdb/exported-client-class-name/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
-
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/snippet.json b/seed/csharp-sdk/imdb/exported-client-class-name/snippet.json index b8fdbd45ba3d..fcfb0f74f660 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/snippet.json +++ b/seed/csharp-sdk/imdb/exported-client-class-name/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new CustomClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new CustomClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index 50e1734c57c3..df598a666c77 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index 6bbf5029779f..8ab69c940103 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Exceptions/NotFoundError.cs similarity index 70% rename from seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs rename to seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Exceptions/NotFoundError.cs index 4565ee4c4962..b90f5220d556 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Exceptions/NotFoundError.cs @@ -4,8 +4,7 @@ namespace SeedApi; /// This exception type will be thrown for any non-2XX API responses. /// [Serializable] -public class MovieDoesNotExistError(string body) - : SeedApiApiException("MovieDoesNotExistError", 404, body) +public class NotFoundError(string body) : CustomClientApiException("NotFoundError", 404, body) { /// /// The body of the response that triggered the exception. diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs deleted file mode 100644 index f2c2ea0b39ae..000000000000 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SeedApi; - -/// -/// This exception type will be thrown for any non-2XX API responses. -/// -[Serializable] -public class MovieDoesNotExistError(string body) - : CustomClientApiException("MovieDoesNotExistError", 404, body) -{ - /// - /// The body of the response that triggered the exception. - /// - public new string Body => body; -} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs index 19fa01d8f85b..56f33c19e567 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/ImdbClient.cs @@ -29,9 +29,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -79,7 +80,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -96,8 +97,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -143,9 +144,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( - JsonUtils.Deserialize(responseBody) - ); + throw new NotFoundError(JsonUtils.Deserialize(responseBody)); } } catch (JsonException) @@ -178,16 +177,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/exported-client-class-name/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Types/Movie.cs diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example1.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example1.cs index 61937b64dc8a..4498ddef78a5 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example2.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example2.cs index 72625d8b76c6..e6f3f8dea28f 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example3.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example3.cs new file mode 100644 index 000000000000..1924e97abca3 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example4.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example4.cs new file mode 100644 index 000000000000..2f14481a2f53 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/reference.md b/seed/csharp-sdk/imdb/extra-dependencies-override/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/reference.md +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
-
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/snippet.json b/seed/csharp-sdk/imdb/extra-dependencies-override/snippet.json index 1d9ade8c9658..9f0ab8270c8a 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/snippet.json +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index 50e1734c57c3..df598a666c77 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index 6bbf5029779f..8ab69c940103 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Exceptions/NotFoundError.cs similarity index 70% rename from seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs rename to seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Exceptions/NotFoundError.cs index 4565ee4c4962..2a2123580519 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Exceptions/NotFoundError.cs @@ -4,8 +4,7 @@ namespace SeedApi; /// This exception type will be thrown for any non-2XX API responses. /// [Serializable] -public class MovieDoesNotExistError(string body) - : SeedApiApiException("MovieDoesNotExistError", 404, body) +public class NotFoundError(string body) : SeedApiApiException("NotFoundError", 404, body) { /// /// The body of the response that triggered the exception. diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/ImdbClient.cs index 27c72e74dec9..812d323c47a2 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/ImdbClient.cs @@ -29,9 +29,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -79,7 +80,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -96,8 +97,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -143,9 +144,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( - JsonUtils.Deserialize(responseBody) - ); + throw new NotFoundError(JsonUtils.Deserialize(responseBody)); } } catch (JsonException) @@ -178,16 +177,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/extra-dependencies-override/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/extra-dependencies-override/src/SeedApi/Types/Movie.cs diff --git a/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example1.cs b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example1.cs index 61937b64dc8a..4498ddef78a5 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example2.cs b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example2.cs index 72625d8b76c6..e6f3f8dea28f 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example3.cs b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example3.cs new file mode 100644 index 000000000000..1924e97abca3 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example4.cs b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example4.cs new file mode 100644 index 000000000000..2f14481a2f53 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/reference.md b/seed/csharp-sdk/imdb/extra-dependencies/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/reference.md +++ b/seed/csharp-sdk/imdb/extra-dependencies/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
-
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/extra-dependencies/snippet.json b/seed/csharp-sdk/imdb/extra-dependencies/snippet.json index 1d9ade8c9658..9f0ab8270c8a 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/snippet.json +++ b/seed/csharp-sdk/imdb/extra-dependencies/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index 50e1734c57c3..df598a666c77 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index 6bbf5029779f..8ab69c940103 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Exceptions/NotFoundError.cs similarity index 70% rename from seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs rename to seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Exceptions/NotFoundError.cs index 4565ee4c4962..2a2123580519 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Exceptions/NotFoundError.cs @@ -4,8 +4,7 @@ namespace SeedApi; /// This exception type will be thrown for any non-2XX API responses. /// [Serializable] -public class MovieDoesNotExistError(string body) - : SeedApiApiException("MovieDoesNotExistError", 404, body) +public class NotFoundError(string body) : SeedApiApiException("NotFoundError", 404, body) { /// /// The body of the response that triggered the exception. diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/ImdbClient.cs index 27c72e74dec9..812d323c47a2 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/ImdbClient.cs @@ -29,9 +29,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -79,7 +80,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -96,8 +97,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -143,9 +144,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( - JsonUtils.Deserialize(responseBody) - ); + throw new NotFoundError(JsonUtils.Deserialize(responseBody)); } } catch (JsonException) @@ -178,16 +177,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/extra-dependencies/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Types/Movie.cs diff --git a/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example1.cs b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example1.cs index 61937b64dc8a..4498ddef78a5 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example2.cs b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example2.cs index 72625d8b76c6..e6f3f8dea28f 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example3.cs b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example3.cs new file mode 100644 index 000000000000..1924e97abca3 --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example4.cs b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example4.cs new file mode 100644 index 000000000000..2f14481a2f53 --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/reference.md b/seed/csharp-sdk/imdb/include-exception-handler/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/reference.md +++ b/seed/csharp-sdk/imdb/include-exception-handler/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
-
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/include-exception-handler/snippet.json b/seed/csharp-sdk/imdb/include-exception-handler/snippet.json index 1d9ade8c9658..9f0ab8270c8a 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/snippet.json +++ b/seed/csharp-sdk/imdb/include-exception-handler/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index 50e1734c57c3..df598a666c77 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index 6bbf5029779f..8ab69c940103 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Exceptions/NotFoundError.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Exceptions/NotFoundError.cs new file mode 100644 index 000000000000..2a2123580519 --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Exceptions/NotFoundError.cs @@ -0,0 +1,13 @@ +namespace SeedApi; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +[Serializable] +public class NotFoundError(string body) : SeedApiApiException("NotFoundError", 404, body) +{ + /// + /// The body of the response that triggered the exception. + /// + public new string Body => body; +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs index ea7b4d8547cf..45b0696728de 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/ImdbClient.cs @@ -40,9 +40,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -94,7 +95,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -114,8 +115,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -163,7 +164,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( + throw new NotFoundError( JsonUtils.Deserialize(responseBody) ); } @@ -200,16 +201,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/include-exception-handler/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Types/Movie.cs diff --git a/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example1.cs b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example1.cs index 61937b64dc8a..4498ddef78a5 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example2.cs b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example2.cs index 72625d8b76c6..e6f3f8dea28f 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example3.cs b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example3.cs new file mode 100644 index 000000000000..1924e97abca3 --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example4.cs b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example4.cs new file mode 100644 index 000000000000..2f14481a2f53 --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/reference.md b/seed/csharp-sdk/imdb/no-custom-config/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/reference.md +++ b/seed/csharp-sdk/imdb/no-custom-config/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
-
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/no-custom-config/snippet.json b/seed/csharp-sdk/imdb/no-custom-config/snippet.json index 1d9ade8c9658..9f0ab8270c8a 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/snippet.json +++ b/seed/csharp-sdk/imdb/no-custom-config/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index 50e1734c57c3..df598a666c77 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index 6bbf5029779f..8ab69c940103 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Exceptions/NotFoundError.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Exceptions/NotFoundError.cs new file mode 100644 index 000000000000..2a2123580519 --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Exceptions/NotFoundError.cs @@ -0,0 +1,13 @@ +namespace SeedApi; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +[Serializable] +public class NotFoundError(string body) : SeedApiApiException("NotFoundError", 404, body) +{ + /// + /// The body of the response that triggered the exception. + /// + public new string Body => body; +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs deleted file mode 100644 index 4565ee4c4962..000000000000 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SeedApi; - -/// -/// This exception type will be thrown for any non-2XX API responses. -/// -[Serializable] -public class MovieDoesNotExistError(string body) - : SeedApiApiException("MovieDoesNotExistError", 404, body) -{ - /// - /// The body of the response that triggered the exception. - /// - public new string Body => body; -} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/ImdbClient.cs index 27c72e74dec9..812d323c47a2 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/ImdbClient.cs @@ -29,9 +29,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -79,7 +80,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -96,8 +97,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -143,9 +144,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( - JsonUtils.Deserialize(responseBody) - ); + throw new NotFoundError(JsonUtils.Deserialize(responseBody)); } } catch (JsonException) @@ -178,16 +177,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/no-custom-config/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Types/Movie.cs diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example1.cs b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example1.cs index 61937b64dc8a..4498ddef78a5 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example1.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example1.cs @@ -10,8 +10,11 @@ public async Task Example1() { } ); - await client.Imdb.GetMovieAsync( - "movieId" + await client.Imdb.CreateMovieAsync( + new CreateMovieRequest { + Title = "title", + Rating = 1.1 + } ); } diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example2.cs b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example2.cs index 72625d8b76c6..e6f3f8dea28f 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example2.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example2.cs @@ -11,7 +11,9 @@ public async Task Example2() { ); await client.Imdb.GetMovieAsync( - "movieId" + new GetMovieImdbRequest { + MovieId = "movieId" + } ); } diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example3.cs b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example3.cs new file mode 100644 index 000000000000..1924e97abca3 --- /dev/null +++ b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example3.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example3() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example4.cs b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example4.cs new file mode 100644 index 000000000000..2f14481a2f53 --- /dev/null +++ b/seed/csharp-sdk/imdb/omit-fern-headers/Snippets/Example4.cs @@ -0,0 +1,20 @@ +using SeedApi; + +public partial class Examples +{ + public async Task Example4() { + var client = new SeedApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { + MovieId = "movieId" + } + ); + } + +} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/reference.md b/seed/csharp-sdk/imdb/omit-fern-headers/reference.md index 48adaf6821e6..74954d9a4412 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/reference.md +++ b/seed/csharp-sdk/imdb/omit-fern-headers/reference.md @@ -54,7 +54,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
-
client.Imdb.GetMovieAsync(movieId) -> WithRawResponseTask<Movie> +
client.Imdb.GetMovieAsync(GetMovieImdbRequest { ... }) -> WithRawResponseTask<Movie>
@@ -67,7 +67,7 @@ await client.Imdb.CreateMovieAsync(new CreateMovieRequest { Title = "title", Rat
```csharp -await client.Imdb.GetMovieAsync("movieId"); +await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); ```
@@ -82,7 +82,7 @@ await client.Imdb.GetMovieAsync("movieId");
-**movieId:** `string` +**request:** `GetMovieImdbRequest`
diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/snippet.json b/seed/csharp-sdk/imdb/omit-fern-headers/snippet.json index 1d9ade8c9658..9f0ab8270c8a 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/snippet.json +++ b/seed/csharp-sdk/imdb/omit-fern-headers/snippet.json @@ -22,7 +22,7 @@ }, "snippet": { "type": "csharp", - "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(\"movieId\");\n" + "client": "using SeedApi;\n\nvar client = new SeedApiClient(\"TOKEN\");\nawait client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = \"movieId\" });\n" } } ] 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 50e1734c57c3..df598a666c77 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 @@ -10,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string requestJson = """ { @@ -28,6 +28,43 @@ public async Task MockServerTest() WireMock .RequestBuilders.Request.Create() .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") + .UsingPost() + .WithBodyAsJson(requestJson) + ) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.CreateMovieAsync( + new CreateMovieRequest { Title = "title", Rating = 1.1 } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string requestJson = """ + { + "title": "title", + "rating": 1.1 + } + """; + + const string mockResponse = """ + "string" + """; + + Server + .Given( + WireMock + .RequestBuilders.Request.Create() + .WithPath("/movies/create-movie") + .WithHeader("Content-Type", "application/json") .UsingPost() .WithBodyAsJson(requestJson) ) 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 6bbf5029779f..8ab69c940103 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 @@ -1,4 +1,5 @@ using NUnit.Framework; +using SeedApi; using SeedApi.Test.Unit.MockServer; using SeedApi.Test.Utils; @@ -9,7 +10,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] - public async Task MockServerTest() + public async Task MockServerTest_1() { const string mockResponse = """ { @@ -28,7 +29,35 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Imdb.GetMovieAsync("movieId"); + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); + JsonAssert.AreEqual(response, mockResponse); + } + + [NUnit.Framework.Test] + public async Task MockServerTest_2() + { + const string mockResponse = """ + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/movies/movieId").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.Imdb.GetMovieAsync( + new GetMovieImdbRequest { MovieId = "movieId" } + ); JsonAssert.AreEqual(response, mockResponse); } } diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Exceptions/NotFoundError.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Exceptions/NotFoundError.cs new file mode 100644 index 000000000000..2a2123580519 --- /dev/null +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Exceptions/NotFoundError.cs @@ -0,0 +1,13 @@ +namespace SeedApi; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +[Serializable] +public class NotFoundError(string body) : SeedApiApiException("NotFoundError", 404, body) +{ + /// + /// The body of the response that triggered the exception. + /// + public new string Body => body; +} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs deleted file mode 100644 index 4565ee4c4962..000000000000 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Exceptions/MovieDoesNotExistError.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SeedApi; - -/// -/// This exception type will be thrown for any non-2XX API responses. -/// -[Serializable] -public class MovieDoesNotExistError(string body) - : SeedApiApiException("MovieDoesNotExistError", 404, body) -{ - /// - /// The body of the response that triggered the exception. - /// - public new string Body => body; -} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/IImdbClient.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/IImdbClient.cs index d98e19911cf1..e1f117299896 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/IImdbClient.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/IImdbClient.cs @@ -12,7 +12,7 @@ WithRawResponseTask CreateMovieAsync( ); WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ); diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/ImdbClient.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/ImdbClient.cs index 27c72e74dec9..812d323c47a2 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/ImdbClient.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/ImdbClient.cs @@ -29,9 +29,10 @@ private async Task> CreateMovieAsyncCore( new JsonRequest { Method = HttpMethod.Post, - Path = "/movies/create-movie", + Path = "movies/create-movie", Body = request, Headers = _headers, + ContentType = "application/json", Options = options, }, cancellationToken @@ -79,7 +80,7 @@ private async Task> CreateMovieAsyncCore( } private async Task> GetMovieAsyncCore( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) @@ -96,8 +97,8 @@ private async Task> GetMovieAsyncCore( { Method = HttpMethod.Get, Path = string.Format( - "/movies/{0}", - ValueConvert.ToPathParameterString(movieId) + "movies/{0}", + ValueConvert.ToPathParameterString(request.MovieId) ), Headers = _headers, Options = options, @@ -143,9 +144,7 @@ private async Task> GetMovieAsyncCore( switch (response.StatusCode) { case 404: - throw new MovieDoesNotExistError( - JsonUtils.Deserialize(responseBody) - ); + throw new NotFoundError(JsonUtils.Deserialize(responseBody)); } } catch (JsonException) @@ -178,16 +177,16 @@ public WithRawResponseTask CreateMovieAsync( } /// - /// await client.Imdb.GetMovieAsync("movieId"); + /// await client.Imdb.GetMovieAsync(new GetMovieImdbRequest { MovieId = "movieId" }); /// public WithRawResponseTask GetMovieAsync( - string movieId, + GetMovieImdbRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { return new WithRawResponseTask( - GetMovieAsyncCore(movieId, options, cancellationToken) + GetMovieAsyncCore(request, options, cancellationToken) ); } } diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs new file mode 100644 index 000000000000..42345d3634a0 --- /dev/null +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Requests/CreateMovieRequest.cs @@ -0,0 +1,20 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record CreateMovieRequest +{ + [JsonPropertyName("title")] + public required string Title { get; set; } + + [JsonPropertyName("rating")] + public required double Rating { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs new file mode 100644 index 000000000000..9f93cc73d24d --- /dev/null +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Requests/GetMovieImdbRequest.cs @@ -0,0 +1,17 @@ +using global::System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[Serializable] +public record GetMovieImdbRequest +{ + [JsonIgnore] + public required string MovieId { get; set; } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Types/CreateMovieRequest.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Types/CreateMovieRequest.cs deleted file mode 100644 index 03b597b013b4..000000000000 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Types/CreateMovieRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using SeedApi.Core; - -namespace SeedApi; - -[Serializable] -public record CreateMovieRequest : IJsonOnDeserialized -{ - [JsonExtensionData] - private readonly IDictionary _extensionData = - new Dictionary(); - - [JsonPropertyName("title")] - public required string Title { get; set; } - - [JsonPropertyName("rating")] - public required double Rating { 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/imdb/omit-fern-headers/src/SeedApi/Imdb/Types/Movie.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Types/Movie.cs similarity index 100% rename from seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Imdb/Types/Movie.cs rename to seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Types/Movie.cs diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/error_codes.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/error_codes.go index 1f09fc784910..9bd087be2c42 100644 --- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/error_codes.go +++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/error_codes.go @@ -9,7 +9,7 @@ import ( var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ 404: func(apiError *core.APIError) error { - return &MovieDoesNotExistError{ + return &NotFoundError{ APIError: apiError, } }, diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/errors.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/errors.go index fc9063230554..cf17d17a5895 100644 --- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/errors.go +++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/errors.go @@ -7,25 +7,26 @@ import ( core "github.com/imdb/fern/all/the/way/in/here/please/core" ) -type MovieDoesNotExistError struct { +// MovieDoesNotExistError +type NotFoundError struct { *core.APIError Body MovieID } -func (m *MovieDoesNotExistError) UnmarshalJSON(data []byte) error { +func (n *NotFoundError) UnmarshalJSON(data []byte) error { var body MovieID if err := json.Unmarshal(data, &body); err != nil { return err } - m.StatusCode = 404 - m.Body = body + n.StatusCode = 404 + n.Body = body return nil } -func (m *MovieDoesNotExistError) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Body) +func (n *NotFoundError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) } -func (m *MovieDoesNotExistError) Unwrap() error { - return m.APIError +func (n *NotFoundError) Unwrap() error { + return n.APIError } diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb.go index bdba7fd4561b..ebaf0c12ebac 100644 --- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb.go +++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb.go @@ -15,35 +15,11 @@ var ( ) type CreateMovieRequest struct { - Title string `json:"title" url:"title"` - Rating float64 `json:"rating" url:"rating"` + Title string `json:"title" url:"-"` + Rating float64 `json:"rating" url:"-"` // 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 *CreateMovieRequest) GetTitle() string { - if c == nil { - return "" - } - return c.Title -} - -func (c *CreateMovieRequest) GetRating() float64 { - if c == nil { - return 0 - } - return c.Rating -} - -func (c *CreateMovieRequest) GetExtraProperties() map[string]interface{} { - if c == nil { - return nil - } - return c.extraProperties } func (c *CreateMovieRequest) require(field *big.Int) { @@ -69,17 +45,11 @@ func (c *CreateMovieRequest) SetRating(rating float64) { func (c *CreateMovieRequest) UnmarshalJSON(data []byte) error { type unmarshaler CreateMovieRequest - var value unmarshaler - if err := json.Unmarshal(data, &value); err != nil { - return err - } - *c = CreateMovieRequest(value) - extraProperties, err := internal.ExtractExtraProperties(data, *c) - if err != nil { + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { return err } - c.extraProperties = extraProperties - c.rawJSON = json.RawMessage(data) + *c = CreateMovieRequest(body) return nil } @@ -94,19 +64,29 @@ func (c *CreateMovieRequest) MarshalJSON() ([]byte, error) { return json.Marshal(explicitMarshaler) } -func (c *CreateMovieRequest) 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 +var ( + getMovieImdbRequestFieldMovieID = big.NewInt(1 << 0) +) + +type GetMovieImdbRequest struct { + MovieID MovieID `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetMovieImdbRequest) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) } - return fmt.Sprintf("%#v", c) + g.explicitFields.Or(g.explicitFields, field) +} + +// SetMovieID sets the MovieID field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetMovieImdbRequest) SetMovieID(movieID MovieID) { + g.MovieID = movieID + g.require(getMovieImdbRequestFieldMovieID) } var ( diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/client.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/client.go index e6b37cb5eba0..8bd4ff419bb7 100644 --- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/client.go +++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/client.go @@ -52,12 +52,12 @@ func (c *Client) CreateMovie( func (c *Client) GetMovie( ctx context.Context, - movieID please.MovieID, + request *please.GetMovieImdbRequest, opts ...option.RequestOption, ) (*please.Movie, error) { response, err := c.WithRawResponse.GetMovie( ctx, - movieID, + request, opts..., ) if err != nil { diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/raw_client.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/raw_client.go index ee00d10e7573..10a9f714c681 100644 --- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/raw_client.go +++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb/raw_client.go @@ -47,6 +47,7 @@ func (r *RawClient) CreateMovie( r.options.ToHeader(), options.ToHeader(), ) + headers.Add("Content-Type", "application/json") var response please.MovieID raw, err := r.caller.Call( ctx, @@ -74,7 +75,7 @@ func (r *RawClient) CreateMovie( func (r *RawClient) GetMovie( ctx context.Context, - movieID please.MovieID, + request *please.GetMovieImdbRequest, opts ...option.RequestOption, ) (*core.Response[*please.Movie], error) { options := core.NewRequestOptions(opts...) @@ -85,7 +86,7 @@ func (r *RawClient) GetMovie( ) endpointURL := internal.EncodeURL( baseURL+"/movies/%v", - movieID, + request.MovieID, ) headers := internal.MergeHeaders( r.options.ToHeader(), diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb_test.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb_test.go index ef6c325ab70e..5add941bb5a6 100644 --- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb_test.go +++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/imdb_test.go @@ -28,64 +28,46 @@ func TestSettersCreateMovieRequest(t *testing.T) { } -func TestGettersCreateMovieRequest(t *testing.T) { - t.Run("GetTitle", func(t *testing.T) { +func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { + t.Run("SetTitle_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var expected string - obj.Title = expected - - // Act & Assert - assert.Equal(t, expected, obj.GetTitle(), "getter should return the property value") - }) + var fernTestValueTitle string - t.Run("GetTitle_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetTitle() // Should return zero value - }) + // Act + obj.SetTitle(fernTestValueTitle) - t.Run("GetRating", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - var expected float64 - obj.Rating = expected + // 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") - // Act & Assert - assert.Equal(t, expected, obj.GetRating(), "getter should return the property value") - }) + // 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") + } - t.Run("GetRating_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetRating() // Should return zero value + // 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 TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { - t.Run("SetTitle_MarksExplicit", func(t *testing.T) { + t.Run("SetRating_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var fernTestValueTitle string + var fernTestValueRating float64 // Act - obj.SetTitle(fernTestValueTitle) + obj.SetRating(fernTestValueRating) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -109,14 +91,28 @@ func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { // It verifies that setting a field via setter allows successful JSON round-trip }) - t.Run("SetRating_MarksExplicit", func(t *testing.T) { +} + +func TestSettersGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID", func(t *testing.T) { + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID + obj.SetMovieID(fernTestValueMovieID) + assert.Equal(t, fernTestValueMovieID, obj.MovieID) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange - obj := &CreateMovieRequest{} - var fernTestValueRating float64 + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID // Act - obj.SetRating(fernTestValueRating) + obj.SetMovieID(fernTestValueMovieID) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -337,39 +333,6 @@ func TestSettersMarkExplicitMovie(t *testing.T) { } -func TestJSONMarshalingCreateMovieRequest(t *testing.T) { - t.Run("MarshalUnmarshal", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - - // 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 CreateMovieRequest - 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 CreateMovieRequest - 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 CreateMovieRequest - err := json.Unmarshal([]byte(`{}`), &obj) - assert.NoError(t, err, "unmarshaling empty object should succeed") - }) -} - func TestJSONMarshalingMovie(t *testing.T) { t.Run("MarshalUnmarshal", func(t *testing.T) { t.Parallel() @@ -403,22 +366,6 @@ func TestJSONMarshalingMovie(t *testing.T) { }) } -func TestStringCreateMovieRequest(t *testing.T) { - t.Run("StringMethod", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - 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 *CreateMovieRequest - result := obj.String() - assert.Equal(t, "", result, "String() should return for nil receiver") - }) -} - func TestStringMovie(t *testing.T) { t.Run("StringMethod", func(t *testing.T) { t.Parallel() @@ -435,29 +382,6 @@ func TestStringMovie(t *testing.T) { }) } -func TestExtraPropertiesCreateMovieRequest(t *testing.T) { - t.Run("GetExtraProperties", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - // 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 *CreateMovieRequest - extraProps := obj.GetExtraProperties() - assert.Nil(t, extraProps, "nil receiver should return nil without panicking") - }) -} - func TestExtraPropertiesMovie(t *testing.T) { t.Run("GetExtraProperties", func(t *testing.T) { t.Parallel() diff --git a/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example1/snippet.go b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example1/snippet.go index 11fbcb8af9cf..037c93c85b04 100644 --- a/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example1/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + please "github.com/imdb/fern/all/the/way/in/here/please" client "github.com/imdb/fern/all/the/way/in/here/please/client" option "github.com/imdb/fern/all/the/way/in/here/please/option" ) @@ -16,8 +17,12 @@ func do() { "", ), ) - client.Imdb.GetMovie( + request := &please.CreateMovieRequest{ + Title: "title", + Rating: 1.1, + } + client.Imdb.CreateMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example2/snippet.go b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example2/snippet.go index 11fbcb8af9cf..4dc02f27616d 100644 --- a/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example2/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + please "github.com/imdb/fern/all/the/way/in/here/please" client "github.com/imdb/fern/all/the/way/in/here/please/client" option "github.com/imdb/fern/all/the/way/in/here/please/option" ) @@ -16,8 +17,11 @@ func do() { "", ), ) + request := &please.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example3/snippet.go b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..4dc02f27616d --- /dev/null +++ b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example3/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + please "github.com/imdb/fern/all/the/way/in/here/please" + client "github.com/imdb/fern/all/the/way/in/here/please/client" + option "github.com/imdb/fern/all/the/way/in/here/please/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &please.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example4/snippet.go b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..4dc02f27616d --- /dev/null +++ b/seed/go-sdk/imdb/deep-package-path/dynamic-snippets/example4/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + please "github.com/imdb/fern/all/the/way/in/here/please" + client "github.com/imdb/fern/all/the/way/in/here/please/client" + option "github.com/imdb/fern/all/the/way/in/here/please/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &please.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/deep-package-path/reference.md b/seed/go-sdk/imdb/deep-package-path/reference.md index 2dcc5a4e2caa..f35bc42dce74 100644 --- a/seed/go-sdk/imdb/deep-package-path/reference.md +++ b/seed/go-sdk/imdb/deep-package-path/reference.md @@ -50,7 +50,15 @@ client.Imdb.CreateMovie(
-**request:** `*please.CreateMovieRequest` +**title:** `string` + +
+
+ +
+
+ +**rating:** `float64`
@@ -75,9 +83,12 @@ client.Imdb.CreateMovie(
```go +request := &please.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } ``` diff --git a/seed/go-sdk/imdb/deep-package-path/snippet.json b/seed/go-sdk/imdb/deep-package-path/snippet.json index e105eadb6b40..422346b54640 100644 --- a/seed/go-sdk/imdb/deep-package-path/snippet.json +++ b/seed/go-sdk/imdb/deep-package-path/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\toption \"github.com/imdb/fern/all/the/way/in/here/please/option\"\n\tpleaseclient \"github.com/imdb/fern/all/the/way/in/here/please/client\"\n)\n\nclient := pleaseclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\"movieId\",\n)\n" + "client": "import (\n\tcontext \"context\"\n\toption \"github.com/imdb/fern/all/the/way/in/here/please/option\"\n\tplease \"github.com/imdb/fern/all/the/way/in/here/please\"\n\tpleaseclient \"github.com/imdb/fern/all/the/way/in/here/please/client\"\n)\n\nclient := pleaseclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\u0026please.GetMovieImdbRequest{\n\t\tMovieID: \"movieId\",\n\t},\n)\n" } } ] diff --git a/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example1/snippet.go b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example1/snippet.go index 4acf6c432216..5dec6a38965e 100644 --- a/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example1/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + fern "github.com/imdb/fern" client "github.com/imdb/fern/client" option "github.com/imdb/fern/option" ) @@ -16,8 +17,12 @@ func do() { "", ), ) - client.Imdb.GetMovie( + request := &fern.CreateMovieRequest{ + Title: "title", + Rating: 1.1, + } + client.Imdb.CreateMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example2/snippet.go b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example2/snippet.go index 4acf6c432216..696ce3d25b4f 100644 --- a/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example2/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + fern "github.com/imdb/fern" client "github.com/imdb/fern/client" option "github.com/imdb/fern/option" ) @@ -16,8 +17,11 @@ func do() { "", ), ) + request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example3/snippet.go b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..696ce3d25b4f --- /dev/null +++ b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example3/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + fern "github.com/imdb/fern" + client "github.com/imdb/fern/client" + option "github.com/imdb/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example4/snippet.go b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..696ce3d25b4f --- /dev/null +++ b/seed/go-sdk/imdb/no-custom-config/dynamic-snippets/example4/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + fern "github.com/imdb/fern" + client "github.com/imdb/fern/client" + option "github.com/imdb/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/no-custom-config/error_codes.go b/seed/go-sdk/imdb/no-custom-config/error_codes.go index 51307b630cd6..5df61197058a 100644 --- a/seed/go-sdk/imdb/no-custom-config/error_codes.go +++ b/seed/go-sdk/imdb/no-custom-config/error_codes.go @@ -9,7 +9,7 @@ import ( var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ 404: func(apiError *core.APIError) error { - return &MovieDoesNotExistError{ + return &NotFoundError{ APIError: apiError, } }, diff --git a/seed/go-sdk/imdb/no-custom-config/errors.go b/seed/go-sdk/imdb/no-custom-config/errors.go index 10d8f396a1f9..1d9f982a96a6 100644 --- a/seed/go-sdk/imdb/no-custom-config/errors.go +++ b/seed/go-sdk/imdb/no-custom-config/errors.go @@ -7,25 +7,26 @@ import ( core "github.com/imdb/fern/core" ) -type MovieDoesNotExistError struct { +// MovieDoesNotExistError +type NotFoundError struct { *core.APIError Body MovieID } -func (m *MovieDoesNotExistError) UnmarshalJSON(data []byte) error { +func (n *NotFoundError) UnmarshalJSON(data []byte) error { var body MovieID if err := json.Unmarshal(data, &body); err != nil { return err } - m.StatusCode = 404 - m.Body = body + n.StatusCode = 404 + n.Body = body return nil } -func (m *MovieDoesNotExistError) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Body) +func (n *NotFoundError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) } -func (m *MovieDoesNotExistError) Unwrap() error { - return m.APIError +func (n *NotFoundError) Unwrap() error { + return n.APIError } diff --git a/seed/go-sdk/imdb/no-custom-config/imdb.go b/seed/go-sdk/imdb/no-custom-config/imdb.go index d9f54cb23277..dc720d61812b 100644 --- a/seed/go-sdk/imdb/no-custom-config/imdb.go +++ b/seed/go-sdk/imdb/no-custom-config/imdb.go @@ -15,35 +15,11 @@ var ( ) type CreateMovieRequest struct { - Title string `json:"title" url:"title"` - Rating float64 `json:"rating" url:"rating"` + Title string `json:"title" url:"-"` + Rating float64 `json:"rating" url:"-"` // 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 *CreateMovieRequest) GetTitle() string { - if c == nil { - return "" - } - return c.Title -} - -func (c *CreateMovieRequest) GetRating() float64 { - if c == nil { - return 0 - } - return c.Rating -} - -func (c *CreateMovieRequest) GetExtraProperties() map[string]interface{} { - if c == nil { - return nil - } - return c.extraProperties } func (c *CreateMovieRequest) require(field *big.Int) { @@ -69,17 +45,11 @@ func (c *CreateMovieRequest) SetRating(rating float64) { func (c *CreateMovieRequest) UnmarshalJSON(data []byte) error { type unmarshaler CreateMovieRequest - var value unmarshaler - if err := json.Unmarshal(data, &value); err != nil { - return err - } - *c = CreateMovieRequest(value) - extraProperties, err := internal.ExtractExtraProperties(data, *c) - if err != nil { + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { return err } - c.extraProperties = extraProperties - c.rawJSON = json.RawMessage(data) + *c = CreateMovieRequest(body) return nil } @@ -94,19 +64,29 @@ func (c *CreateMovieRequest) MarshalJSON() ([]byte, error) { return json.Marshal(explicitMarshaler) } -func (c *CreateMovieRequest) 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 +var ( + getMovieImdbRequestFieldMovieID = big.NewInt(1 << 0) +) + +type GetMovieImdbRequest struct { + MovieID MovieID `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetMovieImdbRequest) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) } - return fmt.Sprintf("%#v", c) + g.explicitFields.Or(g.explicitFields, field) +} + +// SetMovieID sets the MovieID field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetMovieImdbRequest) SetMovieID(movieID MovieID) { + g.MovieID = movieID + g.require(getMovieImdbRequestFieldMovieID) } var ( diff --git a/seed/go-sdk/imdb/no-custom-config/imdb/client.go b/seed/go-sdk/imdb/no-custom-config/imdb/client.go index 439038e6d180..1360dfb924df 100644 --- a/seed/go-sdk/imdb/no-custom-config/imdb/client.go +++ b/seed/go-sdk/imdb/no-custom-config/imdb/client.go @@ -52,12 +52,12 @@ func (c *Client) CreateMovie( func (c *Client) GetMovie( ctx context.Context, - movieID fern.MovieID, + request *fern.GetMovieImdbRequest, opts ...option.RequestOption, ) (*fern.Movie, error) { response, err := c.WithRawResponse.GetMovie( ctx, - movieID, + request, opts..., ) if err != nil { diff --git a/seed/go-sdk/imdb/no-custom-config/imdb/raw_client.go b/seed/go-sdk/imdb/no-custom-config/imdb/raw_client.go index 6646ec52d1d6..efa439acf2cd 100644 --- a/seed/go-sdk/imdb/no-custom-config/imdb/raw_client.go +++ b/seed/go-sdk/imdb/no-custom-config/imdb/raw_client.go @@ -47,6 +47,7 @@ func (r *RawClient) CreateMovie( r.options.ToHeader(), options.ToHeader(), ) + headers.Add("Content-Type", "application/json") var response fern.MovieID raw, err := r.caller.Call( ctx, @@ -74,7 +75,7 @@ func (r *RawClient) CreateMovie( func (r *RawClient) GetMovie( ctx context.Context, - movieID fern.MovieID, + request *fern.GetMovieImdbRequest, opts ...option.RequestOption, ) (*core.Response[*fern.Movie], error) { options := core.NewRequestOptions(opts...) @@ -85,7 +86,7 @@ func (r *RawClient) GetMovie( ) endpointURL := internal.EncodeURL( baseURL+"/movies/%v", - movieID, + request.MovieID, ) headers := internal.MergeHeaders( r.options.ToHeader(), diff --git a/seed/go-sdk/imdb/no-custom-config/imdb_test.go b/seed/go-sdk/imdb/no-custom-config/imdb_test.go index ef6c325ab70e..5add941bb5a6 100644 --- a/seed/go-sdk/imdb/no-custom-config/imdb_test.go +++ b/seed/go-sdk/imdb/no-custom-config/imdb_test.go @@ -28,64 +28,46 @@ func TestSettersCreateMovieRequest(t *testing.T) { } -func TestGettersCreateMovieRequest(t *testing.T) { - t.Run("GetTitle", func(t *testing.T) { +func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { + t.Run("SetTitle_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var expected string - obj.Title = expected - - // Act & Assert - assert.Equal(t, expected, obj.GetTitle(), "getter should return the property value") - }) + var fernTestValueTitle string - t.Run("GetTitle_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetTitle() // Should return zero value - }) + // Act + obj.SetTitle(fernTestValueTitle) - t.Run("GetRating", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - var expected float64 - obj.Rating = expected + // 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") - // Act & Assert - assert.Equal(t, expected, obj.GetRating(), "getter should return the property value") - }) + // 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") + } - t.Run("GetRating_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetRating() // Should return zero value + // 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 TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { - t.Run("SetTitle_MarksExplicit", func(t *testing.T) { + t.Run("SetRating_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var fernTestValueTitle string + var fernTestValueRating float64 // Act - obj.SetTitle(fernTestValueTitle) + obj.SetRating(fernTestValueRating) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -109,14 +91,28 @@ func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { // It verifies that setting a field via setter allows successful JSON round-trip }) - t.Run("SetRating_MarksExplicit", func(t *testing.T) { +} + +func TestSettersGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID", func(t *testing.T) { + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID + obj.SetMovieID(fernTestValueMovieID) + assert.Equal(t, fernTestValueMovieID, obj.MovieID) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange - obj := &CreateMovieRequest{} - var fernTestValueRating float64 + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID // Act - obj.SetRating(fernTestValueRating) + obj.SetMovieID(fernTestValueMovieID) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -337,39 +333,6 @@ func TestSettersMarkExplicitMovie(t *testing.T) { } -func TestJSONMarshalingCreateMovieRequest(t *testing.T) { - t.Run("MarshalUnmarshal", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - - // 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 CreateMovieRequest - 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 CreateMovieRequest - 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 CreateMovieRequest - err := json.Unmarshal([]byte(`{}`), &obj) - assert.NoError(t, err, "unmarshaling empty object should succeed") - }) -} - func TestJSONMarshalingMovie(t *testing.T) { t.Run("MarshalUnmarshal", func(t *testing.T) { t.Parallel() @@ -403,22 +366,6 @@ func TestJSONMarshalingMovie(t *testing.T) { }) } -func TestStringCreateMovieRequest(t *testing.T) { - t.Run("StringMethod", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - 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 *CreateMovieRequest - result := obj.String() - assert.Equal(t, "", result, "String() should return for nil receiver") - }) -} - func TestStringMovie(t *testing.T) { t.Run("StringMethod", func(t *testing.T) { t.Parallel() @@ -435,29 +382,6 @@ func TestStringMovie(t *testing.T) { }) } -func TestExtraPropertiesCreateMovieRequest(t *testing.T) { - t.Run("GetExtraProperties", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - // 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 *CreateMovieRequest - extraProps := obj.GetExtraProperties() - assert.Nil(t, extraProps, "nil receiver should return nil without panicking") - }) -} - func TestExtraPropertiesMovie(t *testing.T) { t.Run("GetExtraProperties", func(t *testing.T) { t.Parallel() diff --git a/seed/go-sdk/imdb/no-custom-config/reference.md b/seed/go-sdk/imdb/no-custom-config/reference.md index 2dfbcf40cdff..e44bf06a8b98 100644 --- a/seed/go-sdk/imdb/no-custom-config/reference.md +++ b/seed/go-sdk/imdb/no-custom-config/reference.md @@ -50,7 +50,15 @@ client.Imdb.CreateMovie(
-**request:** `*fern.CreateMovieRequest` +**title:** `string` + +
+
+ +
+
+ +**rating:** `float64`
@@ -75,9 +83,12 @@ client.Imdb.CreateMovie(
```go +request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } ``` diff --git a/seed/go-sdk/imdb/no-custom-config/snippet.json b/seed/go-sdk/imdb/no-custom-config/snippet.json index 7b770a7ba969..f94b3ef9f691 100644 --- a/seed/go-sdk/imdb/no-custom-config/snippet.json +++ b/seed/go-sdk/imdb/no-custom-config/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/imdb/fern/client\"\n\toption \"github.com/imdb/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\"movieId\",\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/imdb/fern\"\n\tfernclient \"github.com/imdb/fern/client\"\n\toption \"github.com/imdb/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\u0026fern.GetMovieImdbRequest{\n\t\tMovieID: \"movieId\",\n\t},\n)\n" } } ] diff --git a/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example1/snippet.go b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example1/snippet.go index 4acf6c432216..5dec6a38965e 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example1/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + fern "github.com/imdb/fern" client "github.com/imdb/fern/client" option "github.com/imdb/fern/option" ) @@ -16,8 +17,12 @@ func do() { "", ), ) - client.Imdb.GetMovie( + request := &fern.CreateMovieRequest{ + Title: "title", + Rating: 1.1, + } + client.Imdb.CreateMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example2/snippet.go b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example2/snippet.go index 4acf6c432216..696ce3d25b4f 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example2/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + fern "github.com/imdb/fern" client "github.com/imdb/fern/client" option "github.com/imdb/fern/option" ) @@ -16,8 +17,11 @@ func do() { "", ), ) + request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example3/snippet.go b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..696ce3d25b4f --- /dev/null +++ b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example3/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + fern "github.com/imdb/fern" + client "github.com/imdb/fern/client" + option "github.com/imdb/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example4/snippet.go b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..696ce3d25b4f --- /dev/null +++ b/seed/go-sdk/imdb/omit-fern-headers/dynamic-snippets/example4/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + fern "github.com/imdb/fern" + client "github.com/imdb/fern/client" + option "github.com/imdb/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/omit-fern-headers/error_codes.go b/seed/go-sdk/imdb/omit-fern-headers/error_codes.go index 51307b630cd6..5df61197058a 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/error_codes.go +++ b/seed/go-sdk/imdb/omit-fern-headers/error_codes.go @@ -9,7 +9,7 @@ import ( var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ 404: func(apiError *core.APIError) error { - return &MovieDoesNotExistError{ + return &NotFoundError{ APIError: apiError, } }, diff --git a/seed/go-sdk/imdb/omit-fern-headers/errors.go b/seed/go-sdk/imdb/omit-fern-headers/errors.go index 10d8f396a1f9..1d9f982a96a6 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/errors.go +++ b/seed/go-sdk/imdb/omit-fern-headers/errors.go @@ -7,25 +7,26 @@ import ( core "github.com/imdb/fern/core" ) -type MovieDoesNotExistError struct { +// MovieDoesNotExistError +type NotFoundError struct { *core.APIError Body MovieID } -func (m *MovieDoesNotExistError) UnmarshalJSON(data []byte) error { +func (n *NotFoundError) UnmarshalJSON(data []byte) error { var body MovieID if err := json.Unmarshal(data, &body); err != nil { return err } - m.StatusCode = 404 - m.Body = body + n.StatusCode = 404 + n.Body = body return nil } -func (m *MovieDoesNotExistError) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Body) +func (n *NotFoundError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) } -func (m *MovieDoesNotExistError) Unwrap() error { - return m.APIError +func (n *NotFoundError) Unwrap() error { + return n.APIError } diff --git a/seed/go-sdk/imdb/omit-fern-headers/imdb.go b/seed/go-sdk/imdb/omit-fern-headers/imdb.go index d9f54cb23277..dc720d61812b 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/imdb.go +++ b/seed/go-sdk/imdb/omit-fern-headers/imdb.go @@ -15,35 +15,11 @@ var ( ) type CreateMovieRequest struct { - Title string `json:"title" url:"title"` - Rating float64 `json:"rating" url:"rating"` + Title string `json:"title" url:"-"` + Rating float64 `json:"rating" url:"-"` // 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 *CreateMovieRequest) GetTitle() string { - if c == nil { - return "" - } - return c.Title -} - -func (c *CreateMovieRequest) GetRating() float64 { - if c == nil { - return 0 - } - return c.Rating -} - -func (c *CreateMovieRequest) GetExtraProperties() map[string]interface{} { - if c == nil { - return nil - } - return c.extraProperties } func (c *CreateMovieRequest) require(field *big.Int) { @@ -69,17 +45,11 @@ func (c *CreateMovieRequest) SetRating(rating float64) { func (c *CreateMovieRequest) UnmarshalJSON(data []byte) error { type unmarshaler CreateMovieRequest - var value unmarshaler - if err := json.Unmarshal(data, &value); err != nil { - return err - } - *c = CreateMovieRequest(value) - extraProperties, err := internal.ExtractExtraProperties(data, *c) - if err != nil { + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { return err } - c.extraProperties = extraProperties - c.rawJSON = json.RawMessage(data) + *c = CreateMovieRequest(body) return nil } @@ -94,19 +64,29 @@ func (c *CreateMovieRequest) MarshalJSON() ([]byte, error) { return json.Marshal(explicitMarshaler) } -func (c *CreateMovieRequest) 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 +var ( + getMovieImdbRequestFieldMovieID = big.NewInt(1 << 0) +) + +type GetMovieImdbRequest struct { + MovieID MovieID `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetMovieImdbRequest) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) } - return fmt.Sprintf("%#v", c) + g.explicitFields.Or(g.explicitFields, field) +} + +// SetMovieID sets the MovieID field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetMovieImdbRequest) SetMovieID(movieID MovieID) { + g.MovieID = movieID + g.require(getMovieImdbRequestFieldMovieID) } var ( diff --git a/seed/go-sdk/imdb/omit-fern-headers/imdb/client.go b/seed/go-sdk/imdb/omit-fern-headers/imdb/client.go index 439038e6d180..1360dfb924df 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/imdb/client.go +++ b/seed/go-sdk/imdb/omit-fern-headers/imdb/client.go @@ -52,12 +52,12 @@ func (c *Client) CreateMovie( func (c *Client) GetMovie( ctx context.Context, - movieID fern.MovieID, + request *fern.GetMovieImdbRequest, opts ...option.RequestOption, ) (*fern.Movie, error) { response, err := c.WithRawResponse.GetMovie( ctx, - movieID, + request, opts..., ) if err != nil { diff --git a/seed/go-sdk/imdb/omit-fern-headers/imdb/raw_client.go b/seed/go-sdk/imdb/omit-fern-headers/imdb/raw_client.go index 6646ec52d1d6..efa439acf2cd 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/imdb/raw_client.go +++ b/seed/go-sdk/imdb/omit-fern-headers/imdb/raw_client.go @@ -47,6 +47,7 @@ func (r *RawClient) CreateMovie( r.options.ToHeader(), options.ToHeader(), ) + headers.Add("Content-Type", "application/json") var response fern.MovieID raw, err := r.caller.Call( ctx, @@ -74,7 +75,7 @@ func (r *RawClient) CreateMovie( func (r *RawClient) GetMovie( ctx context.Context, - movieID fern.MovieID, + request *fern.GetMovieImdbRequest, opts ...option.RequestOption, ) (*core.Response[*fern.Movie], error) { options := core.NewRequestOptions(opts...) @@ -85,7 +86,7 @@ func (r *RawClient) GetMovie( ) endpointURL := internal.EncodeURL( baseURL+"/movies/%v", - movieID, + request.MovieID, ) headers := internal.MergeHeaders( r.options.ToHeader(), diff --git a/seed/go-sdk/imdb/omit-fern-headers/imdb_test.go b/seed/go-sdk/imdb/omit-fern-headers/imdb_test.go index ef6c325ab70e..5add941bb5a6 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/imdb_test.go +++ b/seed/go-sdk/imdb/omit-fern-headers/imdb_test.go @@ -28,64 +28,46 @@ func TestSettersCreateMovieRequest(t *testing.T) { } -func TestGettersCreateMovieRequest(t *testing.T) { - t.Run("GetTitle", func(t *testing.T) { +func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { + t.Run("SetTitle_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var expected string - obj.Title = expected - - // Act & Assert - assert.Equal(t, expected, obj.GetTitle(), "getter should return the property value") - }) + var fernTestValueTitle string - t.Run("GetTitle_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetTitle() // Should return zero value - }) + // Act + obj.SetTitle(fernTestValueTitle) - t.Run("GetRating", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - var expected float64 - obj.Rating = expected + // 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") - // Act & Assert - assert.Equal(t, expected, obj.GetRating(), "getter should return the property value") - }) + // 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") + } - t.Run("GetRating_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetRating() // Should return zero value + // 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 TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { - t.Run("SetTitle_MarksExplicit", func(t *testing.T) { + t.Run("SetRating_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var fernTestValueTitle string + var fernTestValueRating float64 // Act - obj.SetTitle(fernTestValueTitle) + obj.SetRating(fernTestValueRating) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -109,14 +91,28 @@ func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { // It verifies that setting a field via setter allows successful JSON round-trip }) - t.Run("SetRating_MarksExplicit", func(t *testing.T) { +} + +func TestSettersGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID", func(t *testing.T) { + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID + obj.SetMovieID(fernTestValueMovieID) + assert.Equal(t, fernTestValueMovieID, obj.MovieID) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange - obj := &CreateMovieRequest{} - var fernTestValueRating float64 + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID // Act - obj.SetRating(fernTestValueRating) + obj.SetMovieID(fernTestValueMovieID) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -337,39 +333,6 @@ func TestSettersMarkExplicitMovie(t *testing.T) { } -func TestJSONMarshalingCreateMovieRequest(t *testing.T) { - t.Run("MarshalUnmarshal", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - - // 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 CreateMovieRequest - 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 CreateMovieRequest - 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 CreateMovieRequest - err := json.Unmarshal([]byte(`{}`), &obj) - assert.NoError(t, err, "unmarshaling empty object should succeed") - }) -} - func TestJSONMarshalingMovie(t *testing.T) { t.Run("MarshalUnmarshal", func(t *testing.T) { t.Parallel() @@ -403,22 +366,6 @@ func TestJSONMarshalingMovie(t *testing.T) { }) } -func TestStringCreateMovieRequest(t *testing.T) { - t.Run("StringMethod", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - 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 *CreateMovieRequest - result := obj.String() - assert.Equal(t, "", result, "String() should return for nil receiver") - }) -} - func TestStringMovie(t *testing.T) { t.Run("StringMethod", func(t *testing.T) { t.Parallel() @@ -435,29 +382,6 @@ func TestStringMovie(t *testing.T) { }) } -func TestExtraPropertiesCreateMovieRequest(t *testing.T) { - t.Run("GetExtraProperties", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - // 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 *CreateMovieRequest - extraProps := obj.GetExtraProperties() - assert.Nil(t, extraProps, "nil receiver should return nil without panicking") - }) -} - func TestExtraPropertiesMovie(t *testing.T) { t.Run("GetExtraProperties", func(t *testing.T) { t.Parallel() diff --git a/seed/go-sdk/imdb/omit-fern-headers/reference.md b/seed/go-sdk/imdb/omit-fern-headers/reference.md index 2dfbcf40cdff..e44bf06a8b98 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/reference.md +++ b/seed/go-sdk/imdb/omit-fern-headers/reference.md @@ -50,7 +50,15 @@ client.Imdb.CreateMovie(
-**request:** `*fern.CreateMovieRequest` +**title:** `string` + +
+
+ +
+
+ +**rating:** `float64`
@@ -75,9 +83,12 @@ client.Imdb.CreateMovie(
```go +request := &fern.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } ``` diff --git a/seed/go-sdk/imdb/omit-fern-headers/snippet.json b/seed/go-sdk/imdb/omit-fern-headers/snippet.json index 7b770a7ba969..f94b3ef9f691 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/snippet.json +++ b/seed/go-sdk/imdb/omit-fern-headers/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/imdb/fern/client\"\n\toption \"github.com/imdb/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\"movieId\",\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/imdb/fern\"\n\tfernclient \"github.com/imdb/fern/client\"\n\toption \"github.com/imdb/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\u0026fern.GetMovieImdbRequest{\n\t\tMovieID: \"movieId\",\n\t},\n)\n" } } ] diff --git a/seed/go-sdk/imdb/package-path/dynamic-snippets/example1/snippet.go b/seed/go-sdk/imdb/package-path/dynamic-snippets/example1/snippet.go index ec5da1fe3f0b..678b29c0684b 100644 --- a/seed/go-sdk/imdb/package-path/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/imdb/package-path/dynamic-snippets/example1/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + inhereplease "github.com/imdb/fern/inhereplease" client "github.com/imdb/fern/inhereplease/client" option "github.com/imdb/fern/inhereplease/option" ) @@ -16,8 +17,12 @@ func do() { "", ), ) - client.Imdb.GetMovie( + request := &inhereplease.CreateMovieRequest{ + Title: "title", + Rating: 1.1, + } + client.Imdb.CreateMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/package-path/dynamic-snippets/example2/snippet.go b/seed/go-sdk/imdb/package-path/dynamic-snippets/example2/snippet.go index ec5da1fe3f0b..4795b26261d7 100644 --- a/seed/go-sdk/imdb/package-path/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/imdb/package-path/dynamic-snippets/example2/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + inhereplease "github.com/imdb/fern/inhereplease" client "github.com/imdb/fern/inhereplease/client" option "github.com/imdb/fern/inhereplease/option" ) @@ -16,8 +17,11 @@ func do() { "", ), ) + request := &inhereplease.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/package-path/dynamic-snippets/example3/snippet.go b/seed/go-sdk/imdb/package-path/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..4795b26261d7 --- /dev/null +++ b/seed/go-sdk/imdb/package-path/dynamic-snippets/example3/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + inhereplease "github.com/imdb/fern/inhereplease" + client "github.com/imdb/fern/inhereplease/client" + option "github.com/imdb/fern/inhereplease/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &inhereplease.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/package-path/dynamic-snippets/example4/snippet.go b/seed/go-sdk/imdb/package-path/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..4795b26261d7 --- /dev/null +++ b/seed/go-sdk/imdb/package-path/dynamic-snippets/example4/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + inhereplease "github.com/imdb/fern/inhereplease" + client "github.com/imdb/fern/inhereplease/client" + option "github.com/imdb/fern/inhereplease/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &inhereplease.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/package-path/inhereplease/error_codes.go b/seed/go-sdk/imdb/package-path/inhereplease/error_codes.go index 762d1773f25a..030a650dd0af 100644 --- a/seed/go-sdk/imdb/package-path/inhereplease/error_codes.go +++ b/seed/go-sdk/imdb/package-path/inhereplease/error_codes.go @@ -9,7 +9,7 @@ import ( var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ 404: func(apiError *core.APIError) error { - return &MovieDoesNotExistError{ + return &NotFoundError{ APIError: apiError, } }, diff --git a/seed/go-sdk/imdb/package-path/inhereplease/errors.go b/seed/go-sdk/imdb/package-path/inhereplease/errors.go index f8ff285efaaa..76d3f7db84a5 100644 --- a/seed/go-sdk/imdb/package-path/inhereplease/errors.go +++ b/seed/go-sdk/imdb/package-path/inhereplease/errors.go @@ -7,25 +7,26 @@ import ( core "github.com/imdb/fern/inhereplease/core" ) -type MovieDoesNotExistError struct { +// MovieDoesNotExistError +type NotFoundError struct { *core.APIError Body MovieID } -func (m *MovieDoesNotExistError) UnmarshalJSON(data []byte) error { +func (n *NotFoundError) UnmarshalJSON(data []byte) error { var body MovieID if err := json.Unmarshal(data, &body); err != nil { return err } - m.StatusCode = 404 - m.Body = body + n.StatusCode = 404 + n.Body = body return nil } -func (m *MovieDoesNotExistError) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Body) +func (n *NotFoundError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) } -func (m *MovieDoesNotExistError) Unwrap() error { - return m.APIError +func (n *NotFoundError) Unwrap() error { + return n.APIError } diff --git a/seed/go-sdk/imdb/package-path/inhereplease/imdb.go b/seed/go-sdk/imdb/package-path/inhereplease/imdb.go index 9089a08d9f6c..89cbf0fad7af 100644 --- a/seed/go-sdk/imdb/package-path/inhereplease/imdb.go +++ b/seed/go-sdk/imdb/package-path/inhereplease/imdb.go @@ -15,35 +15,11 @@ var ( ) type CreateMovieRequest struct { - Title string `json:"title" url:"title"` - Rating float64 `json:"rating" url:"rating"` + Title string `json:"title" url:"-"` + Rating float64 `json:"rating" url:"-"` // 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 *CreateMovieRequest) GetTitle() string { - if c == nil { - return "" - } - return c.Title -} - -func (c *CreateMovieRequest) GetRating() float64 { - if c == nil { - return 0 - } - return c.Rating -} - -func (c *CreateMovieRequest) GetExtraProperties() map[string]interface{} { - if c == nil { - return nil - } - return c.extraProperties } func (c *CreateMovieRequest) require(field *big.Int) { @@ -69,17 +45,11 @@ func (c *CreateMovieRequest) SetRating(rating float64) { func (c *CreateMovieRequest) UnmarshalJSON(data []byte) error { type unmarshaler CreateMovieRequest - var value unmarshaler - if err := json.Unmarshal(data, &value); err != nil { - return err - } - *c = CreateMovieRequest(value) - extraProperties, err := internal.ExtractExtraProperties(data, *c) - if err != nil { + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { return err } - c.extraProperties = extraProperties - c.rawJSON = json.RawMessage(data) + *c = CreateMovieRequest(body) return nil } @@ -94,19 +64,29 @@ func (c *CreateMovieRequest) MarshalJSON() ([]byte, error) { return json.Marshal(explicitMarshaler) } -func (c *CreateMovieRequest) 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 +var ( + getMovieImdbRequestFieldMovieID = big.NewInt(1 << 0) +) + +type GetMovieImdbRequest struct { + MovieID MovieID `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetMovieImdbRequest) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) } - return fmt.Sprintf("%#v", c) + g.explicitFields.Or(g.explicitFields, field) +} + +// SetMovieID sets the MovieID field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetMovieImdbRequest) SetMovieID(movieID MovieID) { + g.MovieID = movieID + g.require(getMovieImdbRequestFieldMovieID) } var ( diff --git a/seed/go-sdk/imdb/package-path/inhereplease/imdb/client.go b/seed/go-sdk/imdb/package-path/inhereplease/imdb/client.go index a132e1f1f5e9..9094bd6382d1 100644 --- a/seed/go-sdk/imdb/package-path/inhereplease/imdb/client.go +++ b/seed/go-sdk/imdb/package-path/inhereplease/imdb/client.go @@ -52,12 +52,12 @@ func (c *Client) CreateMovie( func (c *Client) GetMovie( ctx context.Context, - movieID inhereplease.MovieID, + request *inhereplease.GetMovieImdbRequest, opts ...option.RequestOption, ) (*inhereplease.Movie, error) { response, err := c.WithRawResponse.GetMovie( ctx, - movieID, + request, opts..., ) if err != nil { diff --git a/seed/go-sdk/imdb/package-path/inhereplease/imdb/raw_client.go b/seed/go-sdk/imdb/package-path/inhereplease/imdb/raw_client.go index ad50293511de..6d1d0f082d05 100644 --- a/seed/go-sdk/imdb/package-path/inhereplease/imdb/raw_client.go +++ b/seed/go-sdk/imdb/package-path/inhereplease/imdb/raw_client.go @@ -47,6 +47,7 @@ func (r *RawClient) CreateMovie( r.options.ToHeader(), options.ToHeader(), ) + headers.Add("Content-Type", "application/json") var response inhereplease.MovieID raw, err := r.caller.Call( ctx, @@ -74,7 +75,7 @@ func (r *RawClient) CreateMovie( func (r *RawClient) GetMovie( ctx context.Context, - movieID inhereplease.MovieID, + request *inhereplease.GetMovieImdbRequest, opts ...option.RequestOption, ) (*core.Response[*inhereplease.Movie], error) { options := core.NewRequestOptions(opts...) @@ -85,7 +86,7 @@ func (r *RawClient) GetMovie( ) endpointURL := internal.EncodeURL( baseURL+"/movies/%v", - movieID, + request.MovieID, ) headers := internal.MergeHeaders( r.options.ToHeader(), diff --git a/seed/go-sdk/imdb/package-path/inhereplease/imdb_test.go b/seed/go-sdk/imdb/package-path/inhereplease/imdb_test.go index ef6c325ab70e..5add941bb5a6 100644 --- a/seed/go-sdk/imdb/package-path/inhereplease/imdb_test.go +++ b/seed/go-sdk/imdb/package-path/inhereplease/imdb_test.go @@ -28,64 +28,46 @@ func TestSettersCreateMovieRequest(t *testing.T) { } -func TestGettersCreateMovieRequest(t *testing.T) { - t.Run("GetTitle", func(t *testing.T) { +func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { + t.Run("SetTitle_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var expected string - obj.Title = expected - - // Act & Assert - assert.Equal(t, expected, obj.GetTitle(), "getter should return the property value") - }) + var fernTestValueTitle string - t.Run("GetTitle_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetTitle() // Should return zero value - }) + // Act + obj.SetTitle(fernTestValueTitle) - t.Run("GetRating", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - var expected float64 - obj.Rating = expected + // 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") - // Act & Assert - assert.Equal(t, expected, obj.GetRating(), "getter should return the property value") - }) + // 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") + } - t.Run("GetRating_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetRating() // Should return zero value + // 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 TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { - t.Run("SetTitle_MarksExplicit", func(t *testing.T) { + t.Run("SetRating_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var fernTestValueTitle string + var fernTestValueRating float64 // Act - obj.SetTitle(fernTestValueTitle) + obj.SetRating(fernTestValueRating) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -109,14 +91,28 @@ func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { // It verifies that setting a field via setter allows successful JSON round-trip }) - t.Run("SetRating_MarksExplicit", func(t *testing.T) { +} + +func TestSettersGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID", func(t *testing.T) { + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID + obj.SetMovieID(fernTestValueMovieID) + assert.Equal(t, fernTestValueMovieID, obj.MovieID) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange - obj := &CreateMovieRequest{} - var fernTestValueRating float64 + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID // Act - obj.SetRating(fernTestValueRating) + obj.SetMovieID(fernTestValueMovieID) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -337,39 +333,6 @@ func TestSettersMarkExplicitMovie(t *testing.T) { } -func TestJSONMarshalingCreateMovieRequest(t *testing.T) { - t.Run("MarshalUnmarshal", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - - // 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 CreateMovieRequest - 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 CreateMovieRequest - 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 CreateMovieRequest - err := json.Unmarshal([]byte(`{}`), &obj) - assert.NoError(t, err, "unmarshaling empty object should succeed") - }) -} - func TestJSONMarshalingMovie(t *testing.T) { t.Run("MarshalUnmarshal", func(t *testing.T) { t.Parallel() @@ -403,22 +366,6 @@ func TestJSONMarshalingMovie(t *testing.T) { }) } -func TestStringCreateMovieRequest(t *testing.T) { - t.Run("StringMethod", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - 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 *CreateMovieRequest - result := obj.String() - assert.Equal(t, "", result, "String() should return for nil receiver") - }) -} - func TestStringMovie(t *testing.T) { t.Run("StringMethod", func(t *testing.T) { t.Parallel() @@ -435,29 +382,6 @@ func TestStringMovie(t *testing.T) { }) } -func TestExtraPropertiesCreateMovieRequest(t *testing.T) { - t.Run("GetExtraProperties", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - // 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 *CreateMovieRequest - extraProps := obj.GetExtraProperties() - assert.Nil(t, extraProps, "nil receiver should return nil without panicking") - }) -} - func TestExtraPropertiesMovie(t *testing.T) { t.Run("GetExtraProperties", func(t *testing.T) { t.Parallel() diff --git a/seed/go-sdk/imdb/package-path/reference.md b/seed/go-sdk/imdb/package-path/reference.md index e122b009a26f..57bbc7430182 100644 --- a/seed/go-sdk/imdb/package-path/reference.md +++ b/seed/go-sdk/imdb/package-path/reference.md @@ -50,7 +50,15 @@ client.Imdb.CreateMovie(
-**request:** `*inhereplease.CreateMovieRequest` +**title:** `string` + +
+
+ +
+
+ +**rating:** `float64`
@@ -75,9 +83,12 @@ client.Imdb.CreateMovie(
```go +request := &inhereplease.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } ``` diff --git a/seed/go-sdk/imdb/package-path/snippet.json b/seed/go-sdk/imdb/package-path/snippet.json index 6e21c3fd9a50..e01b60f949d4 100644 --- a/seed/go-sdk/imdb/package-path/snippet.json +++ b/seed/go-sdk/imdb/package-path/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tinherepleaseclient \"github.com/imdb/fern/inhereplease/client\"\n\toption \"github.com/imdb/fern/inhereplease/option\"\n)\n\nclient := inherepleaseclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\"movieId\",\n)\n" + "client": "import (\n\tcontext \"context\"\n\tinhereplease \"github.com/imdb/fern/inhereplease\"\n\tinherepleaseclient \"github.com/imdb/fern/inhereplease/client\"\n\toption \"github.com/imdb/fern/inhereplease/option\"\n)\n\nclient := inherepleaseclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\u0026inhereplease.GetMovieImdbRequest{\n\t\tMovieID: \"movieId\",\n\t},\n)\n" } } ] diff --git a/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example1/snippet.go b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example1/snippet.go index ef4f1e05d28c..571cbc203f5b 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example1/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + testPackageName "github.com/imdb/fern" client "github.com/imdb/fern/client" option "github.com/imdb/fern/option" ) @@ -16,8 +17,12 @@ func do() { "", ), ) - client.Imdb.GetMovie( + request := &testPackageName.CreateMovieRequest{ + Title: "title", + Rating: 1.1, + } + client.Imdb.CreateMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example2/snippet.go b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example2/snippet.go index ef4f1e05d28c..5b3021e74f97 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example2/snippet.go @@ -3,6 +3,7 @@ package example import ( context "context" + testPackageName "github.com/imdb/fern" client "github.com/imdb/fern/client" option "github.com/imdb/fern/option" ) @@ -16,8 +17,11 @@ func do() { "", ), ) + request := &testPackageName.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } diff --git a/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example3/snippet.go b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..5b3021e74f97 --- /dev/null +++ b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example3/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + testPackageName "github.com/imdb/fern" + client "github.com/imdb/fern/client" + option "github.com/imdb/fern/option" +) + +func do() { + client := client.NewIMDBClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &testPackageName.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example4/snippet.go b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..5b3021e74f97 --- /dev/null +++ b/seed/go-sdk/imdb/with-wiremock-tests/dynamic-snippets/example4/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + testPackageName "github.com/imdb/fern" + client "github.com/imdb/fern/client" + option "github.com/imdb/fern/option" +) + +func do() { + client := client.NewIMDBClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &testPackageName.GetMovieImdbRequest{ + MovieID: "movieId", + } + client.Imdb.GetMovie( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/imdb/with-wiremock-tests/errors.go b/seed/go-sdk/imdb/with-wiremock-tests/errors.go index aaeadc6fe985..57195d68088f 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/errors.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/errors.go @@ -7,25 +7,26 @@ import ( core "github.com/imdb/fern/core" ) -type MovieDoesNotExistError struct { +// MovieDoesNotExistError +type NotFoundError struct { *core.APIError Body MovieID } -func (m *MovieDoesNotExistError) UnmarshalJSON(data []byte) error { +func (n *NotFoundError) UnmarshalJSON(data []byte) error { var body MovieID if err := json.Unmarshal(data, &body); err != nil { return err } - m.StatusCode = 404 - m.Body = body + n.StatusCode = 404 + n.Body = body return nil } -func (m *MovieDoesNotExistError) MarshalJSON() ([]byte, error) { - return json.Marshal(m.Body) +func (n *NotFoundError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) } -func (m *MovieDoesNotExistError) Unwrap() error { - return m.APIError +func (n *NotFoundError) Unwrap() error { + return n.APIError } diff --git a/seed/go-sdk/imdb/with-wiremock-tests/imdb.go b/seed/go-sdk/imdb/with-wiremock-tests/imdb.go index f8cf58f474ec..78c178c56e11 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/imdb.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/imdb.go @@ -15,35 +15,11 @@ var ( ) type CreateMovieRequest struct { - Title string `json:"title" url:"title"` - Rating float64 `json:"rating" url:"rating"` + Title string `json:"title" url:"-"` + Rating float64 `json:"rating" url:"-"` // 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 *CreateMovieRequest) GetTitle() string { - if c == nil { - return "" - } - return c.Title -} - -func (c *CreateMovieRequest) GetRating() float64 { - if c == nil { - return 0 - } - return c.Rating -} - -func (c *CreateMovieRequest) GetExtraProperties() map[string]interface{} { - if c == nil { - return nil - } - return c.extraProperties } func (c *CreateMovieRequest) require(field *big.Int) { @@ -69,17 +45,11 @@ func (c *CreateMovieRequest) SetRating(rating float64) { func (c *CreateMovieRequest) UnmarshalJSON(data []byte) error { type unmarshaler CreateMovieRequest - var value unmarshaler - if err := json.Unmarshal(data, &value); err != nil { - return err - } - *c = CreateMovieRequest(value) - extraProperties, err := internal.ExtractExtraProperties(data, *c) - if err != nil { + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { return err } - c.extraProperties = extraProperties - c.rawJSON = json.RawMessage(data) + *c = CreateMovieRequest(body) return nil } @@ -94,19 +64,29 @@ func (c *CreateMovieRequest) MarshalJSON() ([]byte, error) { return json.Marshal(explicitMarshaler) } -func (c *CreateMovieRequest) 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 +var ( + getMovieImdbRequestFieldMovieID = big.NewInt(1 << 0) +) + +type GetMovieImdbRequest struct { + MovieID MovieID `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetMovieImdbRequest) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) } - return fmt.Sprintf("%#v", c) + g.explicitFields.Or(g.explicitFields, field) +} + +// SetMovieID sets the MovieID field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetMovieImdbRequest) SetMovieID(movieID MovieID) { + g.MovieID = movieID + g.require(getMovieImdbRequestFieldMovieID) } var ( diff --git a/seed/go-sdk/imdb/with-wiremock-tests/imdb/client.go b/seed/go-sdk/imdb/with-wiremock-tests/imdb/client.go index d1ab3e4b2868..cbd16967d7d7 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/imdb/client.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/imdb/client.go @@ -52,12 +52,12 @@ func (c *Client) CreateMovie( func (c *Client) GetMovie( ctx context.Context, - movieID testPackageName.MovieID, + request *testPackageName.GetMovieImdbRequest, opts ...option.RequestOption, ) (*testPackageName.Movie, error) { response, err := c.WithRawResponse.GetMovie( ctx, - movieID, + request, opts..., ) if err != nil { diff --git a/seed/go-sdk/imdb/with-wiremock-tests/imdb/imdb_test/imdb_test.go b/seed/go-sdk/imdb/with-wiremock-tests/imdb/imdb_test/imdb_test.go index ccba74054a3e..12707e7daae1 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/imdb/imdb_test/imdb_test.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/imdb/imdb_test/imdb_test.go @@ -113,9 +113,12 @@ func TestImdbGetMovieWithWireMock( client := client.NewIMDBClient( option.WithBaseURL(WireMockBaseURL), ) + request := &testPackageName.GetMovieImdbRequest{ + MovieID: "movieId", + } _, invocationErr := client.Imdb.GetMovie( context.TODO(), - "movieId", + request, option.WithHTTPHeader( http.Header{"X-Test-Id": []string{"TestImdbGetMovieWithWireMock"}}, ), diff --git a/seed/go-sdk/imdb/with-wiremock-tests/imdb/raw_client.go b/seed/go-sdk/imdb/with-wiremock-tests/imdb/raw_client.go index 4a2ecddd8124..ab0b3f08afaf 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/imdb/raw_client.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/imdb/raw_client.go @@ -47,6 +47,7 @@ func (r *RawClient) CreateMovie( r.options.ToHeader(), options.ToHeader(), ) + headers.Add("Content-Type", "application/json") var response testPackageName.MovieID raw, err := r.caller.Call( ctx, @@ -74,7 +75,7 @@ func (r *RawClient) CreateMovie( func (r *RawClient) GetMovie( ctx context.Context, - movieID testPackageName.MovieID, + request *testPackageName.GetMovieImdbRequest, opts ...option.RequestOption, ) (*core.Response[*testPackageName.Movie], error) { options := core.NewRequestOptions(opts...) @@ -85,7 +86,7 @@ func (r *RawClient) GetMovie( ) endpointURL := internal.EncodeURL( baseURL+"/movies/%v", - movieID, + request.MovieID, ) headers := internal.MergeHeaders( r.options.ToHeader(), @@ -93,7 +94,7 @@ func (r *RawClient) GetMovie( ) errorCodes := internal.ErrorCodes{ 404: func(apiError *core.APIError) error { - return &testPackageName.MovieDoesNotExistError{ + return &testPackageName.NotFoundError{ APIError: apiError, } }, diff --git a/seed/go-sdk/imdb/with-wiremock-tests/imdb_test.go b/seed/go-sdk/imdb/with-wiremock-tests/imdb_test.go index 761f7085e68b..30ec57e86445 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/imdb_test.go +++ b/seed/go-sdk/imdb/with-wiremock-tests/imdb_test.go @@ -28,64 +28,46 @@ func TestSettersCreateMovieRequest(t *testing.T) { } -func TestGettersCreateMovieRequest(t *testing.T) { - t.Run("GetTitle", func(t *testing.T) { +func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { + t.Run("SetTitle_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var expected string - obj.Title = expected - - // Act & Assert - assert.Equal(t, expected, obj.GetTitle(), "getter should return the property value") - }) + var fernTestValueTitle string - t.Run("GetTitle_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetTitle() // Should return zero value - }) + // Act + obj.SetTitle(fernTestValueTitle) - t.Run("GetRating", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - var expected float64 - obj.Rating = expected + // 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") - // Act & Assert - assert.Equal(t, expected, obj.GetRating(), "getter should return the property value") - }) + // 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") + } - t.Run("GetRating_NilReceiver", func(t *testing.T) { - t.Parallel() - var obj *CreateMovieRequest - // 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.GetRating() // Should return zero value + // 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 TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { - t.Run("SetTitle_MarksExplicit", func(t *testing.T) { + t.Run("SetRating_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange obj := &CreateMovieRequest{} - var fernTestValueTitle string + var fernTestValueRating float64 // Act - obj.SetTitle(fernTestValueTitle) + obj.SetRating(fernTestValueRating) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -109,14 +91,28 @@ func TestSettersMarkExplicitCreateMovieRequest(t *testing.T) { // It verifies that setting a field via setter allows successful JSON round-trip }) - t.Run("SetRating_MarksExplicit", func(t *testing.T) { +} + +func TestSettersGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID", func(t *testing.T) { + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID + obj.SetMovieID(fernTestValueMovieID) + assert.Equal(t, fernTestValueMovieID, obj.MovieID) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetMovieImdbRequest(t *testing.T) { + t.Run("SetMovieID_MarksExplicit", func(t *testing.T) { t.Parallel() // Arrange - obj := &CreateMovieRequest{} - var fernTestValueRating float64 + obj := &GetMovieImdbRequest{} + var fernTestValueMovieID MovieID // Act - obj.SetRating(fernTestValueRating) + obj.SetMovieID(fernTestValueMovieID) // Assert - object with explicitly set field can be marshaled/unmarshaled bytes, err := json.Marshal(obj) @@ -337,39 +333,6 @@ func TestSettersMarkExplicitMovie(t *testing.T) { } -func TestJSONMarshalingCreateMovieRequest(t *testing.T) { - t.Run("MarshalUnmarshal", func(t *testing.T) { - t.Parallel() - // Arrange - obj := &CreateMovieRequest{} - - // 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 CreateMovieRequest - 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 CreateMovieRequest - 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 CreateMovieRequest - err := json.Unmarshal([]byte(`{}`), &obj) - assert.NoError(t, err, "unmarshaling empty object should succeed") - }) -} - func TestJSONMarshalingMovie(t *testing.T) { t.Run("MarshalUnmarshal", func(t *testing.T) { t.Parallel() @@ -403,22 +366,6 @@ func TestJSONMarshalingMovie(t *testing.T) { }) } -func TestStringCreateMovieRequest(t *testing.T) { - t.Run("StringMethod", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - 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 *CreateMovieRequest - result := obj.String() - assert.Equal(t, "", result, "String() should return for nil receiver") - }) -} - func TestStringMovie(t *testing.T) { t.Run("StringMethod", func(t *testing.T) { t.Parallel() @@ -435,29 +382,6 @@ func TestStringMovie(t *testing.T) { }) } -func TestExtraPropertiesCreateMovieRequest(t *testing.T) { - t.Run("GetExtraProperties", func(t *testing.T) { - t.Parallel() - obj := &CreateMovieRequest{} - // 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 *CreateMovieRequest - extraProps := obj.GetExtraProperties() - assert.Nil(t, extraProps, "nil receiver should return nil without panicking") - }) -} - func TestExtraPropertiesMovie(t *testing.T) { t.Run("GetExtraProperties", func(t *testing.T) { t.Parallel() diff --git a/seed/go-sdk/imdb/with-wiremock-tests/reference.md b/seed/go-sdk/imdb/with-wiremock-tests/reference.md index 496065537c35..ba6627e2b28d 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/reference.md +++ b/seed/go-sdk/imdb/with-wiremock-tests/reference.md @@ -50,7 +50,15 @@ client.Imdb.CreateMovie(
-**request:** `*testPackageName.CreateMovieRequest` +**title:** `string` + +
+
+ +
+
+ +**rating:** `float64`
@@ -75,9 +83,12 @@ client.Imdb.CreateMovie(
```go +request := &testPackageName.GetMovieImdbRequest{ + MovieID: "movieId", + } client.Imdb.GetMovie( context.TODO(), - "movieId", + request, ) } ``` diff --git a/seed/go-sdk/imdb/with-wiremock-tests/snippet.json b/seed/go-sdk/imdb/with-wiremock-tests/snippet.json index 5a029705763c..4135188e7bc3 100644 --- a/seed/go-sdk/imdb/with-wiremock-tests/snippet.json +++ b/seed/go-sdk/imdb/with-wiremock-tests/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/imdb/fern/client\"\n\toption \"github.com/imdb/fern/option\"\n)\n\nclient := fernclient.NewIMDBClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\"movieId\",\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/imdb/fern\"\n\tfernclient \"github.com/imdb/fern/client\"\n\toption \"github.com/imdb/fern/option\"\n)\n\nclient := fernclient.NewIMDBClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Imdb.GetMovie(\n\tcontext.TODO(),\n\t\u0026fern.GetMovieImdbRequest{\n\t\tMovieID: \"movieId\",\n\t},\n)\n" } } ] diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/README.md b/seed/java-sdk/imdb/disable-required-property-builder-checks/README.md index f92a1db200fe..c5e93dd80209 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/README.md +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/README.md @@ -56,7 +56,7 @@ Instantiate and use the client with the following: package com.example.usage; import com.seed.api.SeedApiClient; -import com.seed.api.resources.imdb.types.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; public class Example { public static void main(String[] args) { diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/reference.md b/seed/java-sdk/imdb/disable-required-property-builder-checks/reference.md index dd96d08753ef..35f17f656f24 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/reference.md +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/reference.md @@ -48,7 +48,15 @@ client.imdb().createMovie(
-**request:** `CreateMovieRequest` +**title:** `String` + +
+
+ +
+
+ +**rating:** `Double`
@@ -73,7 +81,12 @@ client.imdb().createMovie(
```java -client.imdb().getMovie("movieId"); +client.imdb().getMovie( + "movieId", + GetMovieImdbRequest + .builder() + .build() +); ```
diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/snippet.json b/seed/java-sdk/imdb/disable-required-property-builder-checks/snippet.json index 96d1e11cfb01..7525a55edd13 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/snippet.json +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/snippet.json @@ -1,7 +1,7 @@ { "endpoints": [ { - "example_identifier": "987cdcd2", + "example_identifier": "29893e03", "id": { "method": "POST", "path": "/movies/create-movie", @@ -9,12 +9,12 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.types.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.types.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n" } }, { - "example_identifier": "c0c8e16f", + "example_identifier": "4f9f05f3", "id": { "method": "GET", "path": "/movies/{movieId}", @@ -22,8 +22,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\"movieId\");\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\"movieId\");\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.GetMovieImdbRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\n \"movieId\",\n GetMovieImdbRequest\n .builder()\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.GetMovieImdbRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\n \"movieId\",\n GetMovieImdbRequest\n .builder()\n .build()\n );\n }\n}\n" } } ], diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/errors/MovieDoesNotExistError.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/errors/NotFoundError.java similarity index 60% rename from seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/errors/MovieDoesNotExistError.java rename to seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/errors/NotFoundError.java index d7f82a34c2d0..cfe67f636775 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/errors/MovieDoesNotExistError.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/errors/NotFoundError.java @@ -6,19 +6,19 @@ import com.seed.api.core.SeedApiApiException; import okhttp3.Response; -public final class MovieDoesNotExistError extends SeedApiApiException { +public final class NotFoundError extends SeedApiApiException { /** * The body of the response that triggered the exception. */ private final String body; - public MovieDoesNotExistError(String body) { - super("MovieDoesNotExistError", 404, body); + public NotFoundError(String body) { + super("NotFoundError", 404, body); this.body = body; } - public MovieDoesNotExistError(String body, Response rawResponse) { - super("MovieDoesNotExistError", 404, body, rawResponse); + public NotFoundError(String body, Response rawResponse) { + super("NotFoundError", 404, body, rawResponse); this.body = body; } diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java index b712156967de..79580ecb3d11 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java @@ -5,8 +5,9 @@ import com.seed.api.core.ClientOptions; import com.seed.api.core.RequestOptions; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; import java.util.concurrent.CompletableFuture; public class AsyncImdbClient { @@ -47,4 +48,13 @@ public CompletableFuture getMovie(String movieId) { public CompletableFuture getMovie(String movieId, RequestOptions requestOptions) { return this.rawClient.getMovie(movieId, requestOptions).thenApply(response -> response.body()); } + + public CompletableFuture getMovie(String movieId, GetMovieImdbRequest request) { + return this.rawClient.getMovie(movieId, request).thenApply(response -> response.body()); + } + + public CompletableFuture getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { + return this.rawClient.getMovie(movieId, request, requestOptions).thenApply(response -> response.body()); + } } diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java index d560178b9052..8716d322ec11 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java @@ -11,9 +11,10 @@ import com.seed.api.core.SeedApiApiException; import com.seed.api.core.SeedApiException; import com.seed.api.core.SeedApiHttpResponse; -import com.seed.api.resources.imdb.errors.MovieDoesNotExistError; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.errors.NotFoundError; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; import java.io.IOException; import java.util.concurrent.CompletableFuture; import okhttp3.Call; @@ -48,8 +49,7 @@ public CompletableFuture> createMovie( CreateMovieRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("movies") - .addPathSegments("create-movie"); + .addPathSegments("movies/create-movie"); if (requestOptions != null) { requestOptions.getQueryParameters().forEach((_key, _value) -> { httpUrl.addQueryParameter(_key, _value); @@ -102,10 +102,19 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } public CompletableFuture> getMovie(String movieId) { - return getMovie(movieId, null); + return getMovie(movieId, GetMovieImdbRequest.builder().build()); } public CompletableFuture> getMovie(String movieId, RequestOptions requestOptions) { + return getMovie(movieId, GetMovieImdbRequest.builder().build(), requestOptions); + } + + public CompletableFuture> getMovie(String movieId, GetMovieImdbRequest request) { + return getMovie(movieId, request, null); + } + + public CompletableFuture> getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() .addPathSegments("movies") @@ -115,12 +124,12 @@ public CompletableFuture> getMovie(String movieId, Re httpUrl.addQueryParameter(_key, _value); }); } - Request okhttpRequest = new Request.Builder() + Request.Builder _requestBuilder = new Request.Builder() .url(httpUrl.build()) .method("GET", null) .headers(Headers.of(clientOptions.headers(requestOptions))) - .addHeader("Accept", "application/json") - .build(); + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); OkHttpClient client = clientOptions.httpClient(); if (requestOptions != null && requestOptions.getTimeout().isPresent()) { client = clientOptions.httpClientWithTimeout(requestOptions); @@ -138,7 +147,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO } try { if (response.code() == 404) { - future.completeExceptionally(new MovieDoesNotExistError( + future.completeExceptionally(new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); return; } diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/ImdbClient.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/ImdbClient.java index eae908a6e7d6..0fefe46962aa 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/ImdbClient.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/ImdbClient.java @@ -5,8 +5,9 @@ import com.seed.api.core.ClientOptions; import com.seed.api.core.RequestOptions; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; public class ImdbClient { protected final ClientOptions clientOptions; @@ -46,4 +47,12 @@ public Movie getMovie(String movieId) { public Movie getMovie(String movieId, RequestOptions requestOptions) { return this.rawClient.getMovie(movieId, requestOptions).body(); } + + public Movie getMovie(String movieId, GetMovieImdbRequest request) { + return this.rawClient.getMovie(movieId, request).body(); + } + + public Movie getMovie(String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { + return this.rawClient.getMovie(movieId, request, requestOptions).body(); + } } diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java index e3ef26629250..23c382b4357d 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java @@ -11,9 +11,10 @@ import com.seed.api.core.SeedApiApiException; import com.seed.api.core.SeedApiException; import com.seed.api.core.SeedApiHttpResponse; -import com.seed.api.resources.imdb.errors.MovieDoesNotExistError; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.errors.NotFoundError; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; import java.io.IOException; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -43,8 +44,7 @@ public SeedApiHttpResponse createMovie(CreateMovieRequest request) { public SeedApiHttpResponse createMovie(CreateMovieRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("movies") - .addPathSegments("create-movie"); + .addPathSegments("movies/create-movie"); if (requestOptions != null) { requestOptions.getQueryParameters().forEach((_key, _value) -> { httpUrl.addQueryParameter(_key, _value); @@ -84,10 +84,19 @@ public SeedApiHttpResponse createMovie(CreateMovieRequest request, Reque } public SeedApiHttpResponse getMovie(String movieId) { - return getMovie(movieId, null); + return getMovie(movieId, GetMovieImdbRequest.builder().build()); } public SeedApiHttpResponse getMovie(String movieId, RequestOptions requestOptions) { + return getMovie(movieId, GetMovieImdbRequest.builder().build(), requestOptions); + } + + public SeedApiHttpResponse getMovie(String movieId, GetMovieImdbRequest request) { + return getMovie(movieId, request, null); + } + + public SeedApiHttpResponse getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() .addPathSegments("movies") @@ -97,12 +106,12 @@ public SeedApiHttpResponse getMovie(String movieId, RequestOptions reques httpUrl.addQueryParameter(_key, _value); }); } - Request okhttpRequest = new Request.Builder() + Request.Builder _requestBuilder = new Request.Builder() .url(httpUrl.build()) .method("GET", null) .headers(Headers.of(clientOptions.headers(requestOptions))) - .addHeader("Accept", "application/json") - .build(); + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); OkHttpClient client = clientOptions.httpClient(); if (requestOptions != null && requestOptions.getTimeout().isPresent()) { client = clientOptions.httpClientWithTimeout(requestOptions); @@ -116,7 +125,7 @@ public SeedApiHttpResponse getMovie(String movieId, RequestOptions reques } try { if (response.code() == 404) { - throw new MovieDoesNotExistError( + throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); } } catch (JsonProcessingException ignored) { diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/types/CreateMovieRequest.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/requests/CreateMovieRequest.java similarity index 98% rename from seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/types/CreateMovieRequest.java rename to seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/requests/CreateMovieRequest.java index 104369c5f393..d0b1a22777fd 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/types/CreateMovieRequest.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/requests/CreateMovieRequest.java @@ -1,7 +1,7 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.seed.api.resources.imdb.types; +package com.seed.api.resources.imdb.requests; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/requests/GetMovieImdbRequest.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/requests/GetMovieImdbRequest.java new file mode 100644 index 000000000000..0ad22b6ee05b --- /dev/null +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/requests/GetMovieImdbRequest.java @@ -0,0 +1,69 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.api.resources.imdb.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetMovieImdbRequest.Builder.class) +public final class GetMovieImdbRequest { + private final Map additionalProperties; + + private GetMovieImdbRequest(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetMovieImdbRequest; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(GetMovieImdbRequest other) { + return this; + } + + public GetMovieImdbRequest build() { + return new GetMovieImdbRequest(additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/types/Movie.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/types/Movie.java similarity index 99% rename from seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/types/Movie.java rename to seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/types/Movie.java index b3e430db89d9..069bb45c42fe 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/types/Movie.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/types/Movie.java @@ -1,7 +1,7 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.seed.api.resources.imdb.types; +package com.seed.api.types; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example0.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example0.java index 9615b4c02886..ec1283f2d6f8 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example0.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example0.java @@ -1,7 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; -import com.seed.api.resources.imdb.types.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; public class Example0 { public static void main(String[] args) { diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example1.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example1.java index 8dabe337c6d4..573bd07697a4 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example1.java @@ -1,6 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; public class Example1 { public static void main(String[] args) { @@ -9,6 +10,8 @@ public static void main(String[] args) { .url("https://api.fern.com") .build(); - client.imdb().getMovie("movieId"); + client.imdb() + .createMovie( + CreateMovieRequest.builder().title("title").rating(1.1).build()); } } diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example2.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example2.java index 9308127d2600..6eb2a6917dbc 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example2.java @@ -1,6 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; public class Example2 { public static void main(String[] args) { @@ -9,6 +10,6 @@ public static void main(String[] args) { .url("https://api.fern.com") .build(); - client.imdb().getMovie("movieId"); + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); } } diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example3.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example3.java new file mode 100644 index 000000000000..038aa52cb872 --- /dev/null +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example3.java @@ -0,0 +1,15 @@ +package com.snippets; + +import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; + +public class Example3 { + public static void main(String[] args) { + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); + } +} diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example4.java b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example4.java new file mode 100644 index 000000000000..51a56460da66 --- /dev/null +++ b/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/snippets/Example4.java @@ -0,0 +1,15 @@ +package com.snippets; + +import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; + +public class Example4 { + public static void main(String[] args) { + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); + } +} diff --git a/seed/java-sdk/imdb/flat-package-layout/reference.md b/seed/java-sdk/imdb/flat-package-layout/reference.md index dd96d08753ef..35f17f656f24 100644 --- a/seed/java-sdk/imdb/flat-package-layout/reference.md +++ b/seed/java-sdk/imdb/flat-package-layout/reference.md @@ -48,7 +48,15 @@ client.imdb().createMovie(
-**request:** `CreateMovieRequest` +**title:** `String` + +
+
+ +
+
+ +**rating:** `Double`
@@ -73,7 +81,12 @@ client.imdb().createMovie(
```java -client.imdb().getMovie("movieId"); +client.imdb().getMovie( + "movieId", + GetMovieImdbRequest + .builder() + .build() +); ```
diff --git a/seed/java-sdk/imdb/flat-package-layout/snippet.json b/seed/java-sdk/imdb/flat-package-layout/snippet.json index 9ad7e761d254..a86cd887f36c 100644 --- a/seed/java-sdk/imdb/flat-package-layout/snippet.json +++ b/seed/java-sdk/imdb/flat-package-layout/snippet.json @@ -1,7 +1,7 @@ { "endpoints": [ { - "example_identifier": "987cdcd2", + "example_identifier": "29893e03", "id": { "method": "POST", "path": "/movies/create-movie", @@ -14,7 +14,7 @@ } }, { - "example_identifier": "c0c8e16f", + "example_identifier": "4f9f05f3", "id": { "method": "GET", "path": "/movies/{movieId}", @@ -22,8 +22,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\"movieId\");\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\"movieId\");\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.types.GetMovieImdbRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\n \"movieId\",\n GetMovieImdbRequest\n .builder()\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.types.GetMovieImdbRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\n \"movieId\",\n GetMovieImdbRequest\n .builder()\n .build()\n );\n }\n}\n" } } ], diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncImdbClient.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncImdbClient.java index 614f910e71bb..a6bec541dfa7 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncImdbClient.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncImdbClient.java @@ -6,6 +6,7 @@ import com.seed.api.core.ClientOptions; import com.seed.api.core.RequestOptions; import com.seed.api.types.CreateMovieRequest; +import com.seed.api.types.GetMovieImdbRequest; import com.seed.api.types.Movie; import java.util.concurrent.CompletableFuture; @@ -47,4 +48,13 @@ public CompletableFuture getMovie(String movieId) { public CompletableFuture getMovie(String movieId, RequestOptions requestOptions) { return this.rawClient.getMovie(movieId, requestOptions).thenApply(response -> response.body()); } + + public CompletableFuture getMovie(String movieId, GetMovieImdbRequest request) { + return this.rawClient.getMovie(movieId, request).thenApply(response -> response.body()); + } + + public CompletableFuture getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { + return this.rawClient.getMovie(movieId, request, requestOptions).thenApply(response -> response.body()); + } } diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncRawImdbClient.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncRawImdbClient.java index 1e9bae1dcaba..582c60ba31d1 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncRawImdbClient.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/AsyncRawImdbClient.java @@ -11,8 +11,9 @@ import com.seed.api.core.SeedApiApiException; import com.seed.api.core.SeedApiException; import com.seed.api.core.SeedApiHttpResponse; -import com.seed.api.errors.MovieDoesNotExistError; +import com.seed.api.errors.NotFoundError; import com.seed.api.types.CreateMovieRequest; +import com.seed.api.types.GetMovieImdbRequest; import com.seed.api.types.Movie; import java.io.IOException; import java.util.concurrent.CompletableFuture; @@ -48,8 +49,7 @@ public CompletableFuture> createMovie( CreateMovieRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("movies") - .addPathSegments("create-movie"); + .addPathSegments("movies/create-movie"); if (requestOptions != null) { requestOptions.getQueryParameters().forEach((_key, _value) -> { httpUrl.addQueryParameter(_key, _value); @@ -102,10 +102,19 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } public CompletableFuture> getMovie(String movieId) { - return getMovie(movieId, null); + return getMovie(movieId, GetMovieImdbRequest.builder().build()); } public CompletableFuture> getMovie(String movieId, RequestOptions requestOptions) { + return getMovie(movieId, GetMovieImdbRequest.builder().build(), requestOptions); + } + + public CompletableFuture> getMovie(String movieId, GetMovieImdbRequest request) { + return getMovie(movieId, request, null); + } + + public CompletableFuture> getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() .addPathSegments("movies") @@ -115,12 +124,12 @@ public CompletableFuture> getMovie(String movieId, Re httpUrl.addQueryParameter(_key, _value); }); } - Request okhttpRequest = new Request.Builder() + Request.Builder _requestBuilder = new Request.Builder() .url(httpUrl.build()) .method("GET", null) .headers(Headers.of(clientOptions.headers(requestOptions))) - .addHeader("Accept", "application/json") - .build(); + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); OkHttpClient client = clientOptions.httpClient(); if (requestOptions != null && requestOptions.getTimeout().isPresent()) { client = clientOptions.httpClientWithTimeout(requestOptions); @@ -138,7 +147,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO } try { if (response.code() == 404) { - future.completeExceptionally(new MovieDoesNotExistError( + future.completeExceptionally(new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); return; } diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/ImdbClient.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/ImdbClient.java index bb4460b0ed4c..d40dfaddc09d 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/ImdbClient.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/ImdbClient.java @@ -6,6 +6,7 @@ import com.seed.api.core.ClientOptions; import com.seed.api.core.RequestOptions; import com.seed.api.types.CreateMovieRequest; +import com.seed.api.types.GetMovieImdbRequest; import com.seed.api.types.Movie; public class ImdbClient { @@ -46,4 +47,12 @@ public Movie getMovie(String movieId) { public Movie getMovie(String movieId, RequestOptions requestOptions) { return this.rawClient.getMovie(movieId, requestOptions).body(); } + + public Movie getMovie(String movieId, GetMovieImdbRequest request) { + return this.rawClient.getMovie(movieId, request).body(); + } + + public Movie getMovie(String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { + return this.rawClient.getMovie(movieId, request, requestOptions).body(); + } } diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/RawImdbClient.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/RawImdbClient.java index d493cfa8b781..4255d2b22f16 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/RawImdbClient.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/RawImdbClient.java @@ -11,8 +11,9 @@ import com.seed.api.core.SeedApiApiException; import com.seed.api.core.SeedApiException; import com.seed.api.core.SeedApiHttpResponse; -import com.seed.api.errors.MovieDoesNotExistError; +import com.seed.api.errors.NotFoundError; import com.seed.api.types.CreateMovieRequest; +import com.seed.api.types.GetMovieImdbRequest; import com.seed.api.types.Movie; import java.io.IOException; import okhttp3.Headers; @@ -43,8 +44,7 @@ public SeedApiHttpResponse createMovie(CreateMovieRequest request) { public SeedApiHttpResponse createMovie(CreateMovieRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("movies") - .addPathSegments("create-movie"); + .addPathSegments("movies/create-movie"); if (requestOptions != null) { requestOptions.getQueryParameters().forEach((_key, _value) -> { httpUrl.addQueryParameter(_key, _value); @@ -84,10 +84,19 @@ public SeedApiHttpResponse createMovie(CreateMovieRequest request, Reque } public SeedApiHttpResponse getMovie(String movieId) { - return getMovie(movieId, null); + return getMovie(movieId, GetMovieImdbRequest.builder().build()); } public SeedApiHttpResponse getMovie(String movieId, RequestOptions requestOptions) { + return getMovie(movieId, GetMovieImdbRequest.builder().build(), requestOptions); + } + + public SeedApiHttpResponse getMovie(String movieId, GetMovieImdbRequest request) { + return getMovie(movieId, request, null); + } + + public SeedApiHttpResponse getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() .addPathSegments("movies") @@ -97,12 +106,12 @@ public SeedApiHttpResponse getMovie(String movieId, RequestOptions reques httpUrl.addQueryParameter(_key, _value); }); } - Request okhttpRequest = new Request.Builder() + Request.Builder _requestBuilder = new Request.Builder() .url(httpUrl.build()) .method("GET", null) .headers(Headers.of(clientOptions.headers(requestOptions))) - .addHeader("Accept", "application/json") - .build(); + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); OkHttpClient client = clientOptions.httpClient(); if (requestOptions != null && requestOptions.getTimeout().isPresent()) { client = clientOptions.httpClientWithTimeout(requestOptions); @@ -116,7 +125,7 @@ public SeedApiHttpResponse getMovie(String movieId, RequestOptions reques } try { if (response.code() == 404) { - throw new MovieDoesNotExistError( + throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); } } catch (JsonProcessingException ignored) { diff --git a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/errors/MovieDoesNotExistError.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/errors/NotFoundError.java similarity index 55% rename from seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/errors/MovieDoesNotExistError.java rename to seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/errors/NotFoundError.java index c1b24e741704..cfe67f636775 100644 --- a/seed/java-sdk/imdb/disable-required-property-builder-checks/src/main/java/com/seed/api/resources/imdb/errors/MovieDoesNotExistError.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/errors/NotFoundError.java @@ -1,24 +1,24 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.seed.api.resources.imdb.errors; +package com.seed.api.errors; import com.seed.api.core.SeedApiApiException; import okhttp3.Response; -public final class MovieDoesNotExistError extends SeedApiApiException { +public final class NotFoundError extends SeedApiApiException { /** * The body of the response that triggered the exception. */ private final String body; - public MovieDoesNotExistError(String body) { - super("MovieDoesNotExistError", 404, body); + public NotFoundError(String body) { + super("NotFoundError", 404, body); this.body = body; } - public MovieDoesNotExistError(String body, Response rawResponse) { - super("MovieDoesNotExistError", 404, body, rawResponse); + public NotFoundError(String body, Response rawResponse) { + super("NotFoundError", 404, body, rawResponse); this.body = body; } diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/types/GetMovieImdbRequest.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/types/GetMovieImdbRequest.java new file mode 100644 index 000000000000..84afc5a01722 --- /dev/null +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/seed/api/types/GetMovieImdbRequest.java @@ -0,0 +1,69 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.api.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetMovieImdbRequest.Builder.class) +public final class GetMovieImdbRequest { + private final Map additionalProperties; + + private GetMovieImdbRequest(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetMovieImdbRequest; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(GetMovieImdbRequest other) { + return this; + } + + public GetMovieImdbRequest build() { + return new GetMovieImdbRequest(additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example1.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example1.java index 8dabe337c6d4..3931ffa8ad1e 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example1.java @@ -1,6 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; +import com.seed.api.types.CreateMovieRequest; public class Example1 { public static void main(String[] args) { @@ -9,6 +10,8 @@ public static void main(String[] args) { .url("https://api.fern.com") .build(); - client.imdb().getMovie("movieId"); + client.imdb() + .createMovie( + CreateMovieRequest.builder().title("title").rating(1.1).build()); } } diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example2.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example2.java index 9308127d2600..ed0e6c879161 100644 --- a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example2.java @@ -1,6 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; +import com.seed.api.types.GetMovieImdbRequest; public class Example2 { public static void main(String[] args) { @@ -9,6 +10,6 @@ public static void main(String[] args) { .url("https://api.fern.com") .build(); - client.imdb().getMovie("movieId"); + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); } } diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example3.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example3.java new file mode 100644 index 000000000000..22bf5cf721a0 --- /dev/null +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example3.java @@ -0,0 +1,15 @@ +package com.snippets; + +import com.seed.api.SeedApiClient; +import com.seed.api.types.GetMovieImdbRequest; + +public class Example3 { + public static void main(String[] args) { + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); + } +} diff --git a/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example4.java b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example4.java new file mode 100644 index 000000000000..44d62b1396db --- /dev/null +++ b/seed/java-sdk/imdb/flat-package-layout/src/main/java/com/snippets/Example4.java @@ -0,0 +1,15 @@ +package com.snippets; + +import com.seed.api.SeedApiClient; +import com.seed.api.types.GetMovieImdbRequest; + +public class Example4 { + public static void main(String[] args) { + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); + } +} diff --git a/seed/java-sdk/imdb/omit-fern-headers/README.md b/seed/java-sdk/imdb/omit-fern-headers/README.md index f92a1db200fe..c5e93dd80209 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/README.md +++ b/seed/java-sdk/imdb/omit-fern-headers/README.md @@ -56,7 +56,7 @@ Instantiate and use the client with the following: package com.example.usage; import com.seed.api.SeedApiClient; -import com.seed.api.resources.imdb.types.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; public class Example { public static void main(String[] args) { diff --git a/seed/java-sdk/imdb/omit-fern-headers/reference.md b/seed/java-sdk/imdb/omit-fern-headers/reference.md index dd96d08753ef..35f17f656f24 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/reference.md +++ b/seed/java-sdk/imdb/omit-fern-headers/reference.md @@ -48,7 +48,15 @@ client.imdb().createMovie(
-**request:** `CreateMovieRequest` +**title:** `String` + +
+
+ +
+
+ +**rating:** `Double`
@@ -73,7 +81,12 @@ client.imdb().createMovie(
```java -client.imdb().getMovie("movieId"); +client.imdb().getMovie( + "movieId", + GetMovieImdbRequest + .builder() + .build() +); ```
diff --git a/seed/java-sdk/imdb/omit-fern-headers/snippet.json b/seed/java-sdk/imdb/omit-fern-headers/snippet.json index 96d1e11cfb01..7525a55edd13 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/snippet.json +++ b/seed/java-sdk/imdb/omit-fern-headers/snippet.json @@ -1,7 +1,7 @@ { "endpoints": [ { - "example_identifier": "987cdcd2", + "example_identifier": "29893e03", "id": { "method": "POST", "path": "/movies/create-movie", @@ -9,12 +9,12 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.types.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.types.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.CreateMovieRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().createMovie(\n CreateMovieRequest\n .builder()\n .title(\"title\")\n .rating(1.1)\n .build()\n );\n }\n}\n" } }, { - "example_identifier": "c0c8e16f", + "example_identifier": "4f9f05f3", "id": { "method": "GET", "path": "/movies/{movieId}", @@ -22,8 +22,8 @@ }, "snippet": { "type": "java", - "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\"movieId\");\n }\n}\n", - "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\"movieId\");\n }\n}\n" + "sync_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.GetMovieImdbRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\n \"movieId\",\n GetMovieImdbRequest\n .builder()\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.api.SeedApiClient;\nimport com.seed.api.resources.imdb.requests.GetMovieImdbRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedApiClient client = SeedApiClient\n .builder()\n .token(\"\")\n .build();\n\n client.imdb().getMovie(\n \"movieId\",\n GetMovieImdbRequest\n .builder()\n .build()\n );\n }\n}\n" } } ], diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/errors/MovieDoesNotExistError.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/errors/NotFoundError.java similarity index 55% rename from seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/errors/MovieDoesNotExistError.java rename to seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/errors/NotFoundError.java index c1b24e741704..cfe67f636775 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/errors/MovieDoesNotExistError.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/errors/NotFoundError.java @@ -1,24 +1,24 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.seed.api.resources.imdb.errors; +package com.seed.api.errors; import com.seed.api.core.SeedApiApiException; import okhttp3.Response; -public final class MovieDoesNotExistError extends SeedApiApiException { +public final class NotFoundError extends SeedApiApiException { /** * The body of the response that triggered the exception. */ private final String body; - public MovieDoesNotExistError(String body) { - super("MovieDoesNotExistError", 404, body); + public NotFoundError(String body) { + super("NotFoundError", 404, body); this.body = body; } - public MovieDoesNotExistError(String body, Response rawResponse) { - super("MovieDoesNotExistError", 404, body, rawResponse); + public NotFoundError(String body, Response rawResponse) { + super("NotFoundError", 404, body, rawResponse); this.body = body; } diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java index b712156967de..79580ecb3d11 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncImdbClient.java @@ -5,8 +5,9 @@ import com.seed.api.core.ClientOptions; import com.seed.api.core.RequestOptions; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; import java.util.concurrent.CompletableFuture; public class AsyncImdbClient { @@ -47,4 +48,13 @@ public CompletableFuture getMovie(String movieId) { public CompletableFuture getMovie(String movieId, RequestOptions requestOptions) { return this.rawClient.getMovie(movieId, requestOptions).thenApply(response -> response.body()); } + + public CompletableFuture getMovie(String movieId, GetMovieImdbRequest request) { + return this.rawClient.getMovie(movieId, request).thenApply(response -> response.body()); + } + + public CompletableFuture getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { + return this.rawClient.getMovie(movieId, request, requestOptions).thenApply(response -> response.body()); + } } diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java index d560178b9052..8716d322ec11 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/AsyncRawImdbClient.java @@ -11,9 +11,10 @@ import com.seed.api.core.SeedApiApiException; import com.seed.api.core.SeedApiException; import com.seed.api.core.SeedApiHttpResponse; -import com.seed.api.resources.imdb.errors.MovieDoesNotExistError; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.errors.NotFoundError; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; import java.io.IOException; import java.util.concurrent.CompletableFuture; import okhttp3.Call; @@ -48,8 +49,7 @@ public CompletableFuture> createMovie( CreateMovieRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("movies") - .addPathSegments("create-movie"); + .addPathSegments("movies/create-movie"); if (requestOptions != null) { requestOptions.getQueryParameters().forEach((_key, _value) -> { httpUrl.addQueryParameter(_key, _value); @@ -102,10 +102,19 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } public CompletableFuture> getMovie(String movieId) { - return getMovie(movieId, null); + return getMovie(movieId, GetMovieImdbRequest.builder().build()); } public CompletableFuture> getMovie(String movieId, RequestOptions requestOptions) { + return getMovie(movieId, GetMovieImdbRequest.builder().build(), requestOptions); + } + + public CompletableFuture> getMovie(String movieId, GetMovieImdbRequest request) { + return getMovie(movieId, request, null); + } + + public CompletableFuture> getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() .addPathSegments("movies") @@ -115,12 +124,12 @@ public CompletableFuture> getMovie(String movieId, Re httpUrl.addQueryParameter(_key, _value); }); } - Request okhttpRequest = new Request.Builder() + Request.Builder _requestBuilder = new Request.Builder() .url(httpUrl.build()) .method("GET", null) .headers(Headers.of(clientOptions.headers(requestOptions))) - .addHeader("Accept", "application/json") - .build(); + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); OkHttpClient client = clientOptions.httpClient(); if (requestOptions != null && requestOptions.getTimeout().isPresent()) { client = clientOptions.httpClientWithTimeout(requestOptions); @@ -138,7 +147,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO } try { if (response.code() == 404) { - future.completeExceptionally(new MovieDoesNotExistError( + future.completeExceptionally(new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); return; } diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/ImdbClient.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/ImdbClient.java index eae908a6e7d6..0fefe46962aa 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/ImdbClient.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/ImdbClient.java @@ -5,8 +5,9 @@ import com.seed.api.core.ClientOptions; import com.seed.api.core.RequestOptions; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; public class ImdbClient { protected final ClientOptions clientOptions; @@ -46,4 +47,12 @@ public Movie getMovie(String movieId) { public Movie getMovie(String movieId, RequestOptions requestOptions) { return this.rawClient.getMovie(movieId, requestOptions).body(); } + + public Movie getMovie(String movieId, GetMovieImdbRequest request) { + return this.rawClient.getMovie(movieId, request).body(); + } + + public Movie getMovie(String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { + return this.rawClient.getMovie(movieId, request, requestOptions).body(); + } } diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java index e3ef26629250..23c382b4357d 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/RawImdbClient.java @@ -11,9 +11,10 @@ import com.seed.api.core.SeedApiApiException; import com.seed.api.core.SeedApiException; import com.seed.api.core.SeedApiHttpResponse; -import com.seed.api.resources.imdb.errors.MovieDoesNotExistError; -import com.seed.api.resources.imdb.types.CreateMovieRequest; -import com.seed.api.resources.imdb.types.Movie; +import com.seed.api.errors.NotFoundError; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; +import com.seed.api.types.Movie; import java.io.IOException; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -43,8 +44,7 @@ public SeedApiHttpResponse createMovie(CreateMovieRequest request) { public SeedApiHttpResponse createMovie(CreateMovieRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("movies") - .addPathSegments("create-movie"); + .addPathSegments("movies/create-movie"); if (requestOptions != null) { requestOptions.getQueryParameters().forEach((_key, _value) -> { httpUrl.addQueryParameter(_key, _value); @@ -84,10 +84,19 @@ public SeedApiHttpResponse createMovie(CreateMovieRequest request, Reque } public SeedApiHttpResponse getMovie(String movieId) { - return getMovie(movieId, null); + return getMovie(movieId, GetMovieImdbRequest.builder().build()); } public SeedApiHttpResponse getMovie(String movieId, RequestOptions requestOptions) { + return getMovie(movieId, GetMovieImdbRequest.builder().build(), requestOptions); + } + + public SeedApiHttpResponse getMovie(String movieId, GetMovieImdbRequest request) { + return getMovie(movieId, request, null); + } + + public SeedApiHttpResponse getMovie( + String movieId, GetMovieImdbRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() .addPathSegments("movies") @@ -97,12 +106,12 @@ public SeedApiHttpResponse getMovie(String movieId, RequestOptions reques httpUrl.addQueryParameter(_key, _value); }); } - Request okhttpRequest = new Request.Builder() + Request.Builder _requestBuilder = new Request.Builder() .url(httpUrl.build()) .method("GET", null) .headers(Headers.of(clientOptions.headers(requestOptions))) - .addHeader("Accept", "application/json") - .build(); + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); OkHttpClient client = clientOptions.httpClient(); if (requestOptions != null && requestOptions.getTimeout().isPresent()) { client = clientOptions.httpClientWithTimeout(requestOptions); @@ -116,7 +125,7 @@ public SeedApiHttpResponse getMovie(String movieId, RequestOptions reques } try { if (response.code() == 404) { - throw new MovieDoesNotExistError( + throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); } } catch (JsonProcessingException ignored) { diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/types/CreateMovieRequest.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/requests/CreateMovieRequest.java similarity index 98% rename from seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/types/CreateMovieRequest.java rename to seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/requests/CreateMovieRequest.java index b03e31c40ffb..2e4a32aa21f6 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/types/CreateMovieRequest.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/requests/CreateMovieRequest.java @@ -1,7 +1,7 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.seed.api.resources.imdb.types; +package com.seed.api.resources.imdb.requests; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/requests/GetMovieImdbRequest.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/requests/GetMovieImdbRequest.java new file mode 100644 index 000000000000..0ad22b6ee05b --- /dev/null +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/requests/GetMovieImdbRequest.java @@ -0,0 +1,69 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.api.resources.imdb.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetMovieImdbRequest.Builder.class) +public final class GetMovieImdbRequest { + private final Map additionalProperties; + + private GetMovieImdbRequest(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetMovieImdbRequest; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(GetMovieImdbRequest other) { + return this; + } + + public GetMovieImdbRequest build() { + return new GetMovieImdbRequest(additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/types/Movie.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/types/Movie.java similarity index 99% rename from seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/types/Movie.java rename to seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/types/Movie.java index b7e053cd399b..8a767343b11f 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/resources/imdb/types/Movie.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/seed/api/types/Movie.java @@ -1,7 +1,7 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.seed.api.resources.imdb.types; +package com.seed.api.types; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example0.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example0.java index 9615b4c02886..ec1283f2d6f8 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example0.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example0.java @@ -1,7 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; -import com.seed.api.resources.imdb.types.CreateMovieRequest; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; public class Example0 { public static void main(String[] args) { diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example1.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example1.java index 8dabe337c6d4..573bd07697a4 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example1.java @@ -1,6 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.CreateMovieRequest; public class Example1 { public static void main(String[] args) { @@ -9,6 +10,8 @@ public static void main(String[] args) { .url("https://api.fern.com") .build(); - client.imdb().getMovie("movieId"); + client.imdb() + .createMovie( + CreateMovieRequest.builder().title("title").rating(1.1).build()); } } diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example2.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example2.java index 9308127d2600..6eb2a6917dbc 100644 --- a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example2.java @@ -1,6 +1,7 @@ package com.snippets; import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; public class Example2 { public static void main(String[] args) { @@ -9,6 +10,6 @@ public static void main(String[] args) { .url("https://api.fern.com") .build(); - client.imdb().getMovie("movieId"); + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); } } diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example3.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example3.java new file mode 100644 index 000000000000..038aa52cb872 --- /dev/null +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example3.java @@ -0,0 +1,15 @@ +package com.snippets; + +import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; + +public class Example3 { + public static void main(String[] args) { + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); + } +} diff --git a/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example4.java b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example4.java new file mode 100644 index 000000000000..51a56460da66 --- /dev/null +++ b/seed/java-sdk/imdb/omit-fern-headers/src/main/java/com/snippets/Example4.java @@ -0,0 +1,15 @@ +package com.snippets; + +import com.seed.api.SeedApiClient; +import com.seed.api.resources.imdb.requests.GetMovieImdbRequest; + +public class Example4 { + public static void main(String[] args) { + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.imdb().getMovie("movieId", GetMovieImdbRequest.builder().build()); + } +} diff --git a/seed/openapi/imdb/custom-filename/imdb.yaml b/seed/openapi/imdb/custom-filename/imdb.yaml index a1614de76e3f..5e91ee7f1894 100644 --- a/seed/openapi/imdb/custom-filename/imdb.yaml +++ b/seed/openapi/imdb/custom-filename/imdb.yaml @@ -17,13 +17,34 @@ paths: application/json: schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: string x-fern-availability: pre-release requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateMovieRequest' + type: object + properties: + title: + type: string + examples: + - title + rating: + type: number + format: double + examples: + - 1.1 + required: + - title + - rating + examples: + Example1: + value: + title: title + rating: 1.1 /movies/{movieId}: get: operationId: imdb_getMovie @@ -35,6 +56,9 @@ paths: required: true schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: movieId responses: '200': description: Success @@ -42,8 +66,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Movie' + examples: + Example1: + value: + id: id + title: title + rating: 1.1 '404': - description: MovieDoesNotExistError + description: NotFoundError content: application/json: schema: @@ -62,28 +92,20 @@ components: $ref: '#/components/schemas/MovieId' title: type: string + examples: + - title rating: type: number format: double description: The rating scale is one to five stars + examples: + - 1.1 x-fern-availability: deprecated required: - id - title - rating x-fern-availability: generally-available - CreateMovieRequest: - title: CreateMovieRequest - type: object - properties: - title: - type: string - rating: - type: number - format: double - required: - - title - - rating securitySchemes: Bearer: type: http diff --git a/seed/openapi/imdb/custom-overrides/openapi.yml b/seed/openapi/imdb/custom-overrides/openapi.yml index c74f5a5cd6e0..e9b76057c04f 100644 --- a/seed/openapi/imdb/custom-overrides/openapi.yml +++ b/seed/openapi/imdb/custom-overrides/openapi.yml @@ -19,13 +19,34 @@ paths: application/json: schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: string x-fern-availability: pre-release requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateMovieRequest' + type: object + properties: + title: + type: string + examples: + - title + rating: + type: number + format: double + examples: + - 1.1 + required: + - title + - rating + examples: + Example1: + value: + title: title + rating: 1.1 /movies/{movieId}: get: operationId: imdb_getMovie @@ -37,6 +58,9 @@ paths: required: true schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: movieId responses: '200': description: Success @@ -44,8 +68,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Movie' + examples: + Example1: + value: + id: id + title: title + rating: 1.1 '404': - description: MovieDoesNotExistError + description: NotFoundError content: application/json: schema: @@ -64,28 +94,20 @@ components: $ref: '#/components/schemas/MovieId' title: type: string + examples: + - title rating: type: number format: double description: The rating scale is one to five stars + examples: + - 1.1 x-fern-availability: deprecated required: - id - title - rating x-fern-availability: generally-available - CreateMovieRequest: - title: CreateMovieRequest - type: object - properties: - title: - type: string - rating: - type: number - format: double - required: - - title - - rating securitySchemes: Bearer: type: http diff --git a/seed/openapi/imdb/json-format/openapi.json b/seed/openapi/imdb/json-format/openapi.json index 39dd00fae84c..37e3763029d3 100644 --- a/seed/openapi/imdb/json-format/openapi.json +++ b/seed/openapi/imdb/json-format/openapi.json @@ -20,6 +20,11 @@ "application/json": { "schema": { "$ref": "#/components/schemas/MovieId" + }, + "examples": { + "Example1": { + "value": "string" + } } } } @@ -31,7 +36,34 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateMovieRequest" + "type": "object", + "properties": { + "title": { + "type": "string", + "examples": [ + "title" + ] + }, + "rating": { + "type": "number", + "format": "double", + "examples": [ + 1.1 + ] + } + }, + "required": [ + "title", + "rating" + ] + }, + "examples": { + "Example1": { + "value": { + "title": "title", + "rating": 1.1 + } + } } } } @@ -51,6 +83,11 @@ "required": true, "schema": { "$ref": "#/components/schemas/MovieId" + }, + "examples": { + "Example1": { + "value": "movieId" + } } } ], @@ -61,12 +98,21 @@ "application/json": { "schema": { "$ref": "#/components/schemas/Movie" + }, + "examples": { + "Example1": { + "value": { + "id": "id", + "title": "title", + "rating": 1.1 + } + } } } } }, "404": { - "description": "MovieDoesNotExistError", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -94,12 +140,18 @@ "$ref": "#/components/schemas/MovieId" }, "title": { - "type": "string" + "type": "string", + "examples": [ + "title" + ] }, "rating": { "type": "number", "format": "double", "description": "The rating scale is one to five stars", + "examples": [ + 1.1 + ], "x-fern-availability": "deprecated" } }, @@ -109,23 +161,6 @@ "rating" ], "x-fern-availability": "generally-available" - }, - "CreateMovieRequest": { - "title": "CreateMovieRequest", - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "rating": { - "type": "number", - "format": "double" - } - }, - "required": [ - "title", - "rating" - ] } }, "securitySchemes": { diff --git a/seed/openapi/imdb/no-custom-config/openapi.yml b/seed/openapi/imdb/no-custom-config/openapi.yml index a1614de76e3f..5e91ee7f1894 100644 --- a/seed/openapi/imdb/no-custom-config/openapi.yml +++ b/seed/openapi/imdb/no-custom-config/openapi.yml @@ -17,13 +17,34 @@ paths: application/json: schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: string x-fern-availability: pre-release requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateMovieRequest' + type: object + properties: + title: + type: string + examples: + - title + rating: + type: number + format: double + examples: + - 1.1 + required: + - title + - rating + examples: + Example1: + value: + title: title + rating: 1.1 /movies/{movieId}: get: operationId: imdb_getMovie @@ -35,6 +56,9 @@ paths: required: true schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: movieId responses: '200': description: Success @@ -42,8 +66,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Movie' + examples: + Example1: + value: + id: id + title: title + rating: 1.1 '404': - description: MovieDoesNotExistError + description: NotFoundError content: application/json: schema: @@ -62,28 +92,20 @@ components: $ref: '#/components/schemas/MovieId' title: type: string + examples: + - title rating: type: number format: double description: The rating scale is one to five stars + examples: + - 1.1 x-fern-availability: deprecated required: - id - title - rating x-fern-availability: generally-available - CreateMovieRequest: - title: CreateMovieRequest - type: object - properties: - title: - type: string - rating: - type: number - format: double - required: - - title - - rating securitySchemes: Bearer: type: http diff --git a/seed/openapi/imdb/override/openapi.yml b/seed/openapi/imdb/override/openapi.yml index 55a4ac9971be..026592fb7fb3 100644 --- a/seed/openapi/imdb/override/openapi.yml +++ b/seed/openapi/imdb/override/openapi.yml @@ -17,13 +17,34 @@ paths: application/json: schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: string x-fern-availability: pre-release requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateMovieRequest' + type: object + properties: + title: + type: string + examples: + - title + rating: + type: number + format: double + examples: + - 1.1 + required: + - title + - rating + examples: + Example1: + value: + title: title + rating: 1.1 /movies/{movieId}: get: operationId: imdb_getMovie @@ -35,6 +56,9 @@ paths: required: true schema: $ref: '#/components/schemas/MovieId' + examples: + Example1: + value: movieId responses: '200': description: Success @@ -42,8 +66,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Movie' + examples: + Example1: + value: + id: id + title: title + rating: 1.1 '404': - description: MovieDoesNotExistError + description: NotFoundError content: application/json: schema: @@ -61,28 +91,20 @@ components: $ref: '#/components/schemas/MovieId' title: type: string + examples: + - title rating: type: number format: double description: The rating scale is one to five stars + examples: + - 1.1 x-fern-availability: deprecated required: - id - title - rating x-fern-availability: generally-available - CreateMovieRequest: - title: CreateMovieRequest - type: object - properties: - title: - type: string - rating: - type: number - format: double - required: - - title - - rating securitySchemes: Bearer: type: http diff --git a/seed/php-sdk/imdb/clientName/README.md b/seed/php-sdk/imdb/clientName/README.md index cf597cf4e10d..fa02e9d1fb3b 100644 --- a/seed/php-sdk/imdb/clientName/README.md +++ b/seed/php-sdk/imdb/clientName/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Seed\FernClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new FernClient( token: '', diff --git a/seed/php-sdk/imdb/clientName/reference.md b/seed/php-sdk/imdb/clientName/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/clientName/reference.md +++ b/seed/php-sdk/imdb/clientName/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/clientName/src/Imdb/ImdbClient.php b/seed/php-sdk/imdb/clientName/src/Imdb/ImdbClient.php index 4e85929c88f0..9ffe9e35fe08 100644 --- a/seed/php-sdk/imdb/clientName/src/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/clientName/src/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Seed\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Seed\Imdb\Types\Movie; +use Seed\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/clientName/src/Imdb/Requests/CreateMovieRequest.php similarity index 79% rename from seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/clientName/src/Imdb/Requests/CreateMovieRequest.php index c2658c3de191..c5ce782b81f3 100644 --- a/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/clientName/src/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ title = $values['title']; $this->rating = $values['rating']; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/clientName/src/Imdb/Types/Movie.php b/seed/php-sdk/imdb/clientName/src/Types/Movie.php similarity index 97% rename from seed/php-sdk/imdb/clientName/src/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/clientName/src/Types/Movie.php index 0a77bc1c88c5..6b77cec540ec 100644 --- a/seed/php-sdk/imdb/clientName/src/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/clientName/src/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example1/snippet.php index 754e7fa160fd..53979220c777 100644 --- a/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Seed\FernClient; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new FernClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..754e7fa160fd --- /dev/null +++ b/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..754e7fa160fd --- /dev/null +++ b/seed/php-sdk/imdb/clientName/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/namespace/README.md b/seed/php-sdk/imdb/namespace/README.md index 2b8fde51442a..029de962aef5 100644 --- a/seed/php-sdk/imdb/namespace/README.md +++ b/seed/php-sdk/imdb/namespace/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Fern\SeedClient; -use Fern\Imdb\Types\CreateMovieRequest; +use Fern\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', diff --git a/seed/php-sdk/imdb/namespace/reference.md b/seed/php-sdk/imdb/namespace/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/namespace/reference.md +++ b/seed/php-sdk/imdb/namespace/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/namespace/src/Imdb/ImdbClient.php b/seed/php-sdk/imdb/namespace/src/Imdb/ImdbClient.php index 02d3c8e44ccb..3454a9177f79 100644 --- a/seed/php-sdk/imdb/namespace/src/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/namespace/src/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Fern\Core\Client\RawClient; -use Fern\Imdb\Types\CreateMovieRequest; +use Fern\Imdb\Requests\CreateMovieRequest; use Fern\Exceptions\SeedException; use Fern\Exceptions\SeedApiException; use Fern\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Fern\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Fern\Imdb\Types\Movie; +use Fern\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/namespace/src/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/namespace/src/Imdb/Requests/CreateMovieRequest.php similarity index 79% rename from seed/php-sdk/imdb/namespace/src/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/namespace/src/Imdb/Requests/CreateMovieRequest.php index de15158540a0..b09ff4216f0c 100644 --- a/seed/php-sdk/imdb/namespace/src/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/namespace/src/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ title = $values['title']; $this->rating = $values['rating']; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/namespace/src/Imdb/Types/Movie.php b/seed/php-sdk/imdb/namespace/src/Types/Movie.php similarity index 97% rename from seed/php-sdk/imdb/namespace/src/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/namespace/src/Types/Movie.php index 4954f7b30c4c..8334af7c4a91 100644 --- a/seed/php-sdk/imdb/namespace/src/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/namespace/src/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example1/snippet.php index 5b01fc6fca14..33eb68bde969 100644 --- a/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Fern\SeedClient; +use Fern\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..5b01fc6fca14 --- /dev/null +++ b/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..5b01fc6fca14 --- /dev/null +++ b/seed/php-sdk/imdb/namespace/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/no-custom-config/README.md b/seed/php-sdk/imdb/no-custom-config/README.md index 25020eb6fc61..b23737634441 100644 --- a/seed/php-sdk/imdb/no-custom-config/README.md +++ b/seed/php-sdk/imdb/no-custom-config/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Seed\SeedClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', diff --git a/seed/php-sdk/imdb/no-custom-config/reference.md b/seed/php-sdk/imdb/no-custom-config/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/no-custom-config/reference.md +++ b/seed/php-sdk/imdb/no-custom-config/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/no-custom-config/src/Imdb/ImdbClient.php b/seed/php-sdk/imdb/no-custom-config/src/Imdb/ImdbClient.php index 4e85929c88f0..9ffe9e35fe08 100644 --- a/seed/php-sdk/imdb/no-custom-config/src/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/no-custom-config/src/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Seed\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Seed\Imdb\Types\Movie; +use Seed\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/no-custom-config/src/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/no-custom-config/src/Imdb/Requests/CreateMovieRequest.php similarity index 79% rename from seed/php-sdk/imdb/no-custom-config/src/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/no-custom-config/src/Imdb/Requests/CreateMovieRequest.php index c2658c3de191..c5ce782b81f3 100644 --- a/seed/php-sdk/imdb/no-custom-config/src/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/no-custom-config/src/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ title = $values['title']; $this->rating = $values['rating']; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/no-custom-config/src/Imdb/Types/Movie.php b/seed/php-sdk/imdb/no-custom-config/src/Types/Movie.php similarity index 97% rename from seed/php-sdk/imdb/no-custom-config/src/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/no-custom-config/src/Types/Movie.php index 0a77bc1c88c5..6b77cec540ec 100644 --- a/seed/php-sdk/imdb/no-custom-config/src/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/no-custom-config/src/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example1/snippet.php index 268710306996..08d64102fe5d 100644 --- a/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Seed\SeedClient; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/no-custom-config/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/omit-fern-headers/README.md b/seed/php-sdk/imdb/omit-fern-headers/README.md index 25020eb6fc61..b23737634441 100644 --- a/seed/php-sdk/imdb/omit-fern-headers/README.md +++ b/seed/php-sdk/imdb/omit-fern-headers/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Seed\SeedClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', diff --git a/seed/php-sdk/imdb/omit-fern-headers/reference.md b/seed/php-sdk/imdb/omit-fern-headers/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/omit-fern-headers/reference.md +++ b/seed/php-sdk/imdb/omit-fern-headers/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/ImdbClient.php b/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/ImdbClient.php index 4e85929c88f0..9ffe9e35fe08 100644 --- a/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Seed\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Seed\Imdb\Types\Movie; +use Seed\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/packageName/src/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Requests/CreateMovieRequest.php similarity index 79% rename from seed/php-sdk/imdb/packageName/src/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Requests/CreateMovieRequest.php index c2658c3de191..c5ce782b81f3 100644 --- a/seed/php-sdk/imdb/packageName/src/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ title = $values['title']; $this->rating = $values['rating']; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/packageName/src/Imdb/Types/Movie.php b/seed/php-sdk/imdb/omit-fern-headers/src/Types/Movie.php similarity index 97% rename from seed/php-sdk/imdb/packageName/src/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/omit-fern-headers/src/Types/Movie.php index 0a77bc1c88c5..6b77cec540ec 100644 --- a/seed/php-sdk/imdb/packageName/src/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/omit-fern-headers/src/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example1/snippet.php index 268710306996..08d64102fe5d 100644 --- a/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Seed\SeedClient; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/omit-fern-headers/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/package-path/README.md b/seed/php-sdk/imdb/package-path/README.md index d649c2109086..a5ef4bee8b89 100644 --- a/seed/php-sdk/imdb/package-path/README.md +++ b/seed/php-sdk/imdb/package-path/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Custom\Package\Path\SeedClient; -use Custom\Package\Path\Imdb\Types\CreateMovieRequest; +use Custom\Package\Path\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', diff --git a/seed/php-sdk/imdb/package-path/reference.md b/seed/php-sdk/imdb/package-path/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/package-path/reference.md +++ b/seed/php-sdk/imdb/package-path/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/ImdbClient.php b/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/ImdbClient.php index 1dce7a157dd2..697536fcbf83 100644 --- a/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Custom\Package\Path\Core\Client\RawClient; -use Custom\Package\Path\Imdb\Types\CreateMovieRequest; +use Custom\Package\Path\Imdb\Requests\CreateMovieRequest; use Custom\Package\Path\Exceptions\SeedException; use Custom\Package\Path\Exceptions\SeedApiException; use Custom\Package\Path\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Custom\Package\Path\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Custom\Package\Path\Imdb\Types\Movie; +use Custom\Package\Path\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Requests/CreateMovieRequest.php similarity index 78% rename from seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Requests/CreateMovieRequest.php index 5c6eeb9d2416..e24f86c9c715 100644 --- a/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ title = $values['title']; $this->rating = $values['rating']; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Types/Movie.php b/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Types/Movie.php similarity index 95% rename from seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Types/Movie.php index 78625aa89301..a016cbf3833c 100644 --- a/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/package-path/src/Custom/Package/Path/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example1/snippet.php index 1f20eb78f8a9..889ff8b6c6e4 100644 --- a/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Custom\Package\Path\SeedClient; +use Custom\Package\Path\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..1f20eb78f8a9 --- /dev/null +++ b/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..1f20eb78f8a9 --- /dev/null +++ b/seed/php-sdk/imdb/package-path/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/packageName/README.md b/seed/php-sdk/imdb/packageName/README.md index 21839d8c6d88..71ae39a6f9e0 100644 --- a/seed/php-sdk/imdb/packageName/README.md +++ b/seed/php-sdk/imdb/packageName/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Seed\SeedClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', diff --git a/seed/php-sdk/imdb/packageName/reference.md b/seed/php-sdk/imdb/packageName/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/packageName/reference.md +++ b/seed/php-sdk/imdb/packageName/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/packageName/src/Imdb/ImdbClient.php b/seed/php-sdk/imdb/packageName/src/Imdb/ImdbClient.php index 4e85929c88f0..9ffe9e35fe08 100644 --- a/seed/php-sdk/imdb/packageName/src/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/packageName/src/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Seed\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Seed\Imdb\Types\Movie; +use Seed\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/clientName/src/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/packageName/src/Imdb/Requests/CreateMovieRequest.php similarity index 79% rename from seed/php-sdk/imdb/clientName/src/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/packageName/src/Imdb/Requests/CreateMovieRequest.php index c2658c3de191..c5ce782b81f3 100644 --- a/seed/php-sdk/imdb/clientName/src/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/packageName/src/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ title = $values['title']; $this->rating = $values['rating']; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Types/Movie.php b/seed/php-sdk/imdb/packageName/src/Types/Movie.php similarity index 97% rename from seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/packageName/src/Types/Movie.php index 0a77bc1c88c5..6b77cec540ec 100644 --- a/seed/php-sdk/imdb/omit-fern-headers/src/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/packageName/src/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example1/snippet.php index 268710306996..08d64102fe5d 100644 --- a/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Seed\SeedClient; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/packageName/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/private/README.md b/seed/php-sdk/imdb/private/README.md index 25020eb6fc61..b23737634441 100644 --- a/seed/php-sdk/imdb/private/README.md +++ b/seed/php-sdk/imdb/private/README.md @@ -37,7 +37,7 @@ Instantiate and use the client with the following: namespace Example; use Seed\SeedClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', diff --git a/seed/php-sdk/imdb/private/reference.md b/seed/php-sdk/imdb/private/reference.md index 70d7c6435885..d1c9c62e94a1 100644 --- a/seed/php-sdk/imdb/private/reference.md +++ b/seed/php-sdk/imdb/private/reference.md @@ -47,7 +47,15 @@ $client->imdb->createMovie(
-**$request:** `CreateMovieRequest` +**$title:** `string` + +
+
+ +
+
+ +**$rating:** `float`
diff --git a/seed/php-sdk/imdb/private/src/Imdb/ImdbClient.php b/seed/php-sdk/imdb/private/src/Imdb/ImdbClient.php index 4e85929c88f0..9ffe9e35fe08 100644 --- a/seed/php-sdk/imdb/private/src/Imdb/ImdbClient.php +++ b/seed/php-sdk/imdb/private/src/Imdb/ImdbClient.php @@ -4,7 +4,7 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; -use Seed\Imdb\Types\CreateMovieRequest; +use Seed\Imdb\Requests\CreateMovieRequest; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; @@ -12,7 +12,7 @@ use Seed\Core\Json\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; -use Seed\Imdb\Types\Movie; +use Seed\Types\Movie; class ImdbClient { @@ -73,7 +73,7 @@ public function createMovie(CreateMovieRequest $request, ?array $options = null) $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/create-movie", + path: "movies/create-movie", method: HttpMethod::POST, body: $request, ), @@ -120,7 +120,7 @@ public function getMovie(string $movieId, ?array $options = null): ?Movie $response = $this->client->sendRequest( new JsonApiRequest( baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', - path: "/movies/{$movieId}", + path: "movies/{$movieId}", method: HttpMethod::GET, ), $options, diff --git a/seed/php-sdk/imdb/private/src/Imdb/Types/CreateMovieRequest.php b/seed/php-sdk/imdb/private/src/Imdb/Requests/CreateMovieRequest.php similarity index 89% rename from seed/php-sdk/imdb/private/src/Imdb/Types/CreateMovieRequest.php rename to seed/php-sdk/imdb/private/src/Imdb/Requests/CreateMovieRequest.php index cd037dccb9e9..670c2facd354 100644 --- a/seed/php-sdk/imdb/private/src/Imdb/Types/CreateMovieRequest.php +++ b/seed/php-sdk/imdb/private/src/Imdb/Requests/CreateMovieRequest.php @@ -1,6 +1,6 @@ _setField('rating'); return $this; } - - /** - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } } diff --git a/seed/php-sdk/imdb/private/src/Imdb/Types/Movie.php b/seed/php-sdk/imdb/private/src/Types/Movie.php similarity index 98% rename from seed/php-sdk/imdb/private/src/Imdb/Types/Movie.php rename to seed/php-sdk/imdb/private/src/Types/Movie.php index 5422f4513fea..e3e52e0cc52a 100644 --- a/seed/php-sdk/imdb/private/src/Imdb/Types/Movie.php +++ b/seed/php-sdk/imdb/private/src/Types/Movie.php @@ -1,6 +1,6 @@ ', diff --git a/seed/php-sdk/imdb/private/src/dynamic-snippets/example1/snippet.php b/seed/php-sdk/imdb/private/src/dynamic-snippets/example1/snippet.php index 268710306996..08d64102fe5d 100644 --- a/seed/php-sdk/imdb/private/src/dynamic-snippets/example1/snippet.php +++ b/seed/php-sdk/imdb/private/src/dynamic-snippets/example1/snippet.php @@ -3,6 +3,7 @@ namespace Example; use Seed\SeedClient; +use Seed\Imdb\Requests\CreateMovieRequest; $client = new SeedClient( token: '', @@ -10,6 +11,9 @@ 'baseUrl' => 'https://api.fern.com', ], ); -$client->imdb->getMovie( - 'movieId', +$client->imdb->createMovie( + new CreateMovieRequest([ + 'title' => 'title', + 'rating' => 1.1, + ]), ); diff --git a/seed/php-sdk/imdb/private/src/dynamic-snippets/example3/snippet.php b/seed/php-sdk/imdb/private/src/dynamic-snippets/example3/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/private/src/dynamic-snippets/example3/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/imdb/private/src/dynamic-snippets/example4/snippet.php b/seed/php-sdk/imdb/private/src/dynamic-snippets/example4/snippet.php new file mode 100644 index 000000000000..268710306996 --- /dev/null +++ b/seed/php-sdk/imdb/private/src/dynamic-snippets/example4/snippet.php @@ -0,0 +1,15 @@ +', + options: [ + 'baseUrl' => 'https://api.fern.com', + ], +); +$client->imdb->getMovie( + 'movieId', +); diff --git a/seed/php-sdk/seed.yml b/seed/php-sdk/seed.yml index 100b491d3b99..b4916f6de66d 100644 --- a/seed/php-sdk/seed.yml +++ b/seed/php-sdk/seed.yml @@ -223,7 +223,6 @@ allowedFailures: - variables # Java-specific fixture for staged builder ordering - java-staged-builder-ordering - - server-sent-events-openapi - any-auth - circular-references - file-upload diff --git a/seed/php-sdk/server-sent-event-examples/reference.md b/seed/php-sdk/server-sent-event-examples/reference.md index e6df8754ee51..33acdb1e826c 100644 --- a/seed/php-sdk/server-sent-event-examples/reference.md +++ b/seed/php-sdk/server-sent-event-examples/reference.md @@ -1,6 +1,6 @@ # Reference ## Completions -
$client->completions->stream($request) +
$client->completions->stream($request) -> SseStream
@@ -44,7 +44,7 @@ $client->completions->stream(
-
$client->completions->streamEvents($request) +
$client->completions->streamEvents($request) -> SseStream
@@ -88,7 +88,7 @@ $client->completions->streamEvents(
-
$client->completions->streamEventsDiscriminantInData($request) +
$client->completions->streamEventsDiscriminantInData($request) -> SseStream
@@ -132,7 +132,7 @@ $client->completions->streamEventsDiscriminantInData(
-
$client->completions->streamEventsContextProtocol($request) +
$client->completions->streamEventsContextProtocol($request) -> SseStream
diff --git a/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php b/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php index d5e928e63184..6175f2daaeb6 100644 --- a/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php +++ b/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php @@ -5,14 +5,19 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; use Seed\Completions\Requests\StreamCompletionRequest; +use Seed\Core\Client\SseStream; +use Seed\Completions\Types\StreamedCompletion; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; use Seed\Core\Client\HttpMethod; use Psr\Http\Client\ClientExceptionInterface; use Seed\Completions\Requests\StreamEventsRequest; +use Seed\Completions\Types\StreamEvent; use Seed\Completions\Requests\StreamEventsDiscriminantInDataRequest; +use Seed\Completions\Types\StreamEventDiscriminantInData; use Seed\Completions\Requests\StreamEventsContextProtocolRequest; +use Seed\Completions\Types\StreamEventContextProtocol; class CompletionsClient { @@ -60,10 +65,11 @@ public function __construct( * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function stream(StreamCompletionRequest $request, ?array $options = null): void + public function stream(StreamCompletionRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -77,6 +83,9 @@ public function stream(StreamCompletionRequest $request, ?array $options = null) $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamedCompletion::fromJson($data), terminator: '[[DONE]]'); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -97,10 +106,11 @@ public function stream(StreamCompletionRequest $request, ?array $options = null) * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamEvents(StreamEventsRequest $request, ?array $options = null): void + public function streamEvents(StreamEventsRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -114,6 +124,9 @@ public function streamEvents(StreamEventsRequest $request, ?array $options = nul $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamEvent::fromJson($data), terminator: '[DONE]'); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -134,10 +147,11 @@ public function streamEvents(StreamEventsRequest $request, ?array $options = nul * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamEventsDiscriminantInData(StreamEventsDiscriminantInDataRequest $request, ?array $options = null): void + public function streamEventsDiscriminantInData(StreamEventsDiscriminantInDataRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -151,6 +165,9 @@ public function streamEventsDiscriminantInData(StreamEventsDiscriminantInDataReq $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamEventDiscriminantInData::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -171,10 +188,11 @@ public function streamEventsDiscriminantInData(StreamEventsDiscriminantInDataReq * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamEventsContextProtocol(StreamEventsContextProtocolRequest $request, ?array $options = null): void + public function streamEventsContextProtocol(StreamEventsContextProtocolRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -188,6 +206,9 @@ public function streamEventsContextProtocol(StreamEventsContextProtocolRequest $ $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamEventContextProtocol::fromJson($data), terminator: '[DONE]'); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Client/JsonStream.php b/seed/php-sdk/server-sent-event-examples/src/Core/Client/JsonStream.php new file mode 100644 index 000000000000..d5325e9f6a45 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Client/JsonStream.php @@ -0,0 +1,39 @@ + + */ +class JsonStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per line with the raw + * JSON payload string. + * @param ?string $terminator Optional sentinel line that ends the stream + * when received. Pass `null` to read until EOF. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = null, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Json, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Client/SseEvent.php b/seed/php-sdk/server-sent-event-examples/src/Core/Client/SseEvent.php new file mode 100644 index 000000000000..61ff24ba4aa7 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Client/SseEvent.php @@ -0,0 +1,32 @@ + + */ +class SseStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per dispatched event + * with the raw `data:` payload string (newline-joined for multi-line frames). + * @param ?string $terminator Optional sentinel payload that ends the stream + * when received. Defaults to '[DONE]', a common SSE convention. + * Pass `null` to disable terminator handling. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = '[DONE]', + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + self::validateContentType($response); + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Sse, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } + + /** + * Iterates the stream yielding both the deserialized payload and the + * accompanying SSE metadata (event type, id, retry). Use this when you + * need the event field (e.g. for event-typed unions) or `Last-Event-ID` + * for resumption logic. + * + * For data-only iteration, use this object directly as an iterable: + * `foreach ($stream as $event) { ... }`. + * + * @return Generator> + */ + public function events(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield new SseEvent( + data: $this->deserialize($raw['data']), + event: $raw['event'], + id: $raw['id'], + retry: $raw['retry'], + ); + } + } + + /** + * Validates that the response's Content-Type matches an SSE stream. + * + * Per WHATWG, the SSE wire format is always UTF-8; we reject explicit + * non-UTF-8 charset parameters rather than risk silent mojibake. A missing + * Content-Type header is tolerated — some servers omit it on streaming + * responses — but a wrong media type or wrong charset always throws. + */ + private static function validateContentType(ResponseInterface $response): void + { + $contentType = $response->getHeaderLine('Content-Type'); + if ($contentType === '') { + return; + } + $parts = explode(';', $contentType); + $mediaType = strtolower(trim($parts[0])); + if ($mediaType !== 'text/event-stream') { + throw new RuntimeException( + "Expected Content-Type 'text/event-stream' for SSE response, got '{$mediaType}'", + ); + } + foreach (array_slice($parts, 1) as $param) { + $param = trim($param); + if (stripos($param, 'charset=') !== 0) { + continue; + } + $charset = strtolower(trim(substr($param, 8), " \"'")); + if ($charset !== '' && $charset !== 'utf-8' && $charset !== 'utf8') { + throw new RuntimeException( + "Unsupported SSE charset '{$charset}'; per the WHATWG spec only UTF-8 is permitted", + ); + } + } + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Client/Stream.php b/seed/php-sdk/server-sent-event-examples/src/Core/Client/Stream.php new file mode 100644 index 000000000000..f7616c33b07e --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Client/Stream.php @@ -0,0 +1,287 @@ + + */ +class Stream implements IteratorAggregate +{ + public const DEFAULT_MAX_BUFFER_SIZE = 1_048_576; + + private const READ_CHUNK_SIZE = 8192; + private const UTF8_BOM = "\xEF\xBB\xBF"; + + private StreamInterface $body; + + /** @var Closure(string): T */ + private Closure $deserializer; + + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per frame with the raw payload string. + * For text streams, the deserializer is typically `fn(string $line) => $line`. + * @param StreamFormat $format Framing strategy for the stream. + * @param ?string $terminator Optional sentinel value that ends the stream when matched + * against the raw frame payload (e.g. '[DONE]'). + * @param int $maxBufferSize Maximum size in bytes for the line buffer or a single SSE + * event's accumulated `data` field. Exceeding this throws `RuntimeException` to + * guard against pathological streams. Defaults to 1 MiB. + */ + protected function __construct( + ResponseInterface $response, + Closure $deserializer, + private readonly StreamFormat $format = StreamFormat::Sse, + private readonly ?string $terminator = null, + private readonly int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + $this->body = $response->getBody(); + $this->deserializer = $deserializer; + } + + /** + * Iteration is one-shot: PSR-7 bodies are forward-only, so re-iterating the + * same `Stream` instance yields nothing useful. + * + * @return Generator + */ + public function getIterator(): Generator + { + return match ($this->format) { + StreamFormat::Sse => $this->iterateSse(), + StreamFormat::Json => $this->iterateDelimited(), + StreamFormat::Text => $this->iterateText(), + }; + } + + /** + * Applies the configured deserializer to a single raw frame payload. + * Available to subclasses (notably `SseStream::events()`) so they can + * construct typed envelopes without accessing the closure directly. + * + * @return T + */ + protected function deserialize(string $raw): mixed + { + return ($this->deserializer)($raw); + } + + /** + * @return Generator + */ + private function iterateSse(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield ($this->deserializer)($raw['data']); + } + } + + /** + * Iterates the SSE stream yielding raw envelopes with WHATWG metadata fields + * intact. Yields plain associative arrays — not `SseEvent` objects — so the + * data-only iteration path doesn't pay the allocation cost; `SseStream::events()` + * constructs the public `SseEvent` on top. + * + * Per WHATWG: the `id:` field persists across events within this iteration; + * the configured `terminator`, if present, ends iteration when matched against + * `data`. + * + * @internal + * @return Generator + */ + protected function iterateRawSseEvents(): Generator + { + $dataBuffer = ''; + $eventType = ''; + $lastEventId = ''; + $retry = null; + foreach ($this->readLines() as $line) { + if ($line === '') { + if ($dataBuffer === '') { + continue; + } + $payload = substr($dataBuffer, 0, -1); + $dataBuffer = ''; + if ($this->terminator !== null && $payload === $this->terminator) { + return; + } + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + // Per WHATWG: do NOT reset lastEventId between events. + $eventType = ''; + $retry = null; + continue; + } + if (str_starts_with($line, ':')) { + continue; + } + $colonPos = strpos($line, ':'); + if ($colonPos === false) { + if ($line === 'data') { + $dataBuffer = $this->appendWithinCap($dataBuffer, "\n"); + } + continue; + } + $field = substr($line, 0, $colonPos); + $value = substr($line, $colonPos + 1); + if (str_starts_with($value, ' ')) { + $value = substr($value, 1); + } + switch ($field) { + case 'data': + $dataBuffer = $this->appendWithinCap($dataBuffer, $value . "\n"); + break; + case 'event': + $eventType = $value; + break; + case 'id': + // WHATWG: ignore IDs that contain a NULL byte. + if (!str_contains($value, "\0")) { + $lastEventId = $value; + } + break; + case 'retry': + // WHATWG: ignore the value if it isn't a base-10 integer. + if ($value !== '' && ctype_digit($value)) { + $retry = (int) $value; + } + break; + } + } + // Flush a trailing event that lacked a closing blank line. + if ($dataBuffer !== '') { + $payload = substr($dataBuffer, 0, -1); + if ($this->terminator === null || $payload !== $this->terminator) { + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + } + } + } + + /** + * @return Generator + */ + private function iterateDelimited(): Generator + { + foreach ($this->readLines() as $line) { + if ($line === '') { + continue; + } + if ($this->terminator !== null && $line === $this->terminator) { + return; + } + yield ($this->deserializer)($line); + } + } + + /** + * @return Generator + */ + private function iterateText(): Generator + { + foreach ($this->readLines() as $line) { + yield ($this->deserializer)($line); + } + } + + /** + * Reads the response body and yields complete lines, normalizing CRLF/CR + * to LF per the WHATWG SSE spec. Strips a single UTF-8 BOM if present at + * the start of the stream (WHATWG §9.2.4). Trailing partial content + * (without a terminating newline) is emitted as a final line. + * + * `$pendingCr` tracks whether the prior chunk's last byte was `\r`, so a + * `\r\n` sequence split across a read boundary collapses to one terminator + * instead of two. + * + * @return Generator + */ + private function readLines(): Generator + { + $buffer = ''; + $pendingCr = false; + $bomChecked = false; + while (!$this->body->eof()) { + $chunk = $this->body->read(self::READ_CHUNK_SIZE); + if ($chunk === '') { + continue; + } + if ($pendingCr && $chunk[0] === "\n") { + $chunk = substr($chunk, 1); + } + if (str_contains($chunk, "\r")) { + $pendingCr = str_ends_with($chunk, "\r"); + $chunk = str_replace(["\r\n", "\r"], "\n", $chunk); + } else { + $pendingCr = false; + } + $buffer = $this->appendWithinCap($buffer, $chunk); + if (!$bomChecked && strlen($buffer) >= 3) { + $bomChecked = true; + if (str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + } + while (($lfPos = strpos($buffer, "\n")) !== false) { + yield substr($buffer, 0, $lfPos); + $buffer = substr($buffer, $lfPos + 1); + } + } + // BOM may not have been checked yet if the entire body was < 3 bytes. + if (!$bomChecked && str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + if ($buffer !== '') { + yield $buffer; + } + } + + /** + * Appends $suffix to $buffer, throwing `RuntimeException` if the resulting + * size would exceed `maxBufferSize`. + */ + private function appendWithinCap(string $buffer, string $suffix): string + { + if (strlen($buffer) + strlen($suffix) > $this->maxBufferSize) { + throw new RuntimeException( + "Stream buffer would exceed maximum size of {$this->maxBufferSize} bytes", + ); + } + return $buffer . $suffix; + } + + public function __destruct() + { + try { + $this->body->close(); + } catch (\Throwable) { + // Best effort — the body may already be closed by the consumer. + } + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Client/StreamFormat.php b/seed/php-sdk/server-sent-event-examples/src/Core/Client/StreamFormat.php new file mode 100644 index 000000000000..2db924fefd94 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Client/StreamFormat.php @@ -0,0 +1,13 @@ + + */ +class TextStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: fn (string $line): string => $line, + format: StreamFormat::Text, + terminator: null, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Core/Client/StreamTest.php b/seed/php-sdk/server-sent-event-examples/tests/Core/Client/StreamTest.php new file mode 100644 index 000000000000..ed6ae35d9119 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Core/Client/StreamTest.php @@ -0,0 +1,297 @@ + $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseConcatenatesMultilineDataWithNewlines(): void + { + $body = "data: line one\ndata: line two\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["line one\nline two"], iterator_to_array($stream, false)); + } + + public function testSseStripsLeadingSpaceFromFieldValues(): void + { + // SSE spec: a single leading space in the field value is stripped. + $body = "data: with-leading-space\ndata:no-leading-space\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["with-leading-space\nno-leading-space"], iterator_to_array($stream, false)); + } + + public function testSseIgnoresCommentLines(): void + { + $body = ": this is a comment\ndata: payload\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['payload'], iterator_to_array($stream, false)); + } + + public function testSseTerminatorEndsIterationCleanly(): void + { + $body = "data: first\n\ndata: [DONE]\n\ndata: never-yielded\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, '[DONE]'); + + $this->assertSame(['first'], iterator_to_array($stream, false)); + } + + public function testSseNormalizesCrlfAndLoneCr(): void + { + $body = "data: a\r\n\r\ndata: b\rdata: c\r\r"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['a', "b\nc"], iterator_to_array($stream, false)); + } + + public function testSseDispatchesTrailingEventWithoutBlankLine(): void + { + $body = "data: incomplete"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['incomplete'], iterator_to_array($stream, false)); + } + + public function testSseAppliesDeserializerOncePerEvent(): void + { + $body = "data: {\"n\":1}\n\ndata: {\"n\":2}\n\n"; + $stream = new SseStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2]], iterator_to_array($stream, false)); + } + + public function testSseEventsExposesEventIdAndRetryMetadata(): void + { + $body = "event: chat\nid: msg-1\nretry: 5000\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertCount(1, $events); + $this->assertInstanceOf(SseEvent::class, $events[0]); + $this->assertSame('hi', $events[0]->data); + $this->assertSame('chat', $events[0]->event); + $this->assertSame('msg-1', $events[0]->id); + $this->assertSame(5000, $events[0]->retry); + } + + public function testSseEventsPersistsLastEventIdAcrossEventsPerSpec(): void + { + // Per WHATWG SSE: once an `id:` is set it persists across subsequent + // events until explicitly overridden. + $body = "id: persistent\ndata: a\n\ndata: b\n\nid: replaced\ndata: c\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['persistent', 'persistent', 'replaced'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresIdContainingNullByte(): void + { + $body = "id: ok\ndata: first\n\nid: bad\0id\ndata: second\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['ok', 'ok'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresNonIntegerRetry(): void + { + $body = "retry: not-a-number\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertNull($events[0]->retry); + } + + public function testSseConstructorRejectsNonSseContentType(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/text\/event-stream/'); + + new SseStream( + self::response('data: x\n\n', contentType: 'application/json'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorRejectsNonUtf8Charset(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/charset/i'); + + new SseStream( + self::response('data: x\n\n', contentType: 'text/event-stream; charset=iso-8859-1'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorAcceptsUtf8CharsetParameter(): void + { + $stream = new SseStream( + self::response("data: hi\n\n", contentType: 'text/event-stream; charset=UTF-8'), + fn (string $d): string => $d, + null, + ); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testSseConstructorToleratesMissingContentTypeHeader(): void + { + $response = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withBody(\Http\Discovery\Psr17FactoryDiscovery::findStreamFactory()->createStream("data: hi\n\n")); + + $stream = new SseStream($response, fn (string $d): string => $d, null); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testStreamThrowsWhenLineBufferExceedsMaxSize(): void + { + $bigLine = str_repeat('A', 200) . "\n"; + $stream = new SseStream( + self::response("data: " . $bigLine . "\n"), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testStreamThrowsBeforeAccumulatingPastMaxBufferOnLongRunningSseEvent(): void + { + // Each line is well under the 64-byte cap; the cumulative `data:` + // append must trip the check before the buffer balloons. + $manyDataLines = ''; + for ($i = 0; $i < 50; $i++) { + $manyDataLines .= "data: chunk-$i\n"; + } + $stream = new SseStream( + self::response($manyDataLines), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testSseStripsUtf8BomFromStartOfStream(): void + { + // WHATWG §9.2.4 requires a leading U+FEFF to be dropped. + $body = "\xEF\xBB\xBFdata: hello\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseStripsBomOnlyAtVeryStart(): void + { + $body = "data: first\n\ndata: \xEF\xBB\xBFsecond\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['first', "\xEF\xBB\xBFsecond"], iterator_to_array($stream, false)); + } + + public function testJsonStreamYieldsOnePerLine(): void + { + $body = "{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2], ['n' => 3]], iterator_to_array($stream, false)); + } + + public function testJsonStreamSkipsEmptyLines(): void + { + $body = "{\"a\":1}\n\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['a' => 1], ['a' => 2]], iterator_to_array($stream, false)); + } + + public function testJsonStreamTerminatorEndsIteration(): void + { + $body = "{\"a\":1}\n[DONE]\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), '[DONE]'); + + $this->assertSame([['a' => 1]], iterator_to_array($stream, false)); + } + + /** + * @return \Closure(string): array + */ + private static function jsonDecoder(): \Closure + { + return static function (string $raw): array { + /** @var array $decoded */ + $decoded = json_decode($raw, true); + return $decoded; + }; + } + + public function testTextStreamYieldsRawLines(): void + { + $body = "alpha\nbeta\ngamma\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', 'beta', 'gamma'], iterator_to_array($stream, false)); + } + + public function testTextStreamPreservesEmptyLines(): void + { + $body = "alpha\n\nbeta\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', '', 'beta'], iterator_to_array($stream, false)); + } + + /** + * Build a PSR-7 ResponseInterface with the given body. The Content-Type + * defaults to `text/event-stream` so SSE tests just work; pass an override + * for the validation-error cases. + */ + private static function response( + string $body, + string $contentType = 'text/event-stream', + ): ResponseInterface { + return \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withHeader('Content-Type', $contentType) + ->withBody( + \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory() + ->createStream($body), + ); + } +} diff --git a/seed/php-sdk/server-sent-events-openapi/reference.md b/seed/php-sdk/server-sent-events-openapi/reference.md index 8402b1dcaffb..dd6d73ca808e 100644 --- a/seed/php-sdk/server-sent-events-openapi/reference.md +++ b/seed/php-sdk/server-sent-events-openapi/reference.md @@ -1,5 +1,5 @@ # Reference -
$client->streamProtocolNoCollision($request) +
$client->streamProtocolNoCollision($request) -> SseStream
@@ -55,7 +55,7 @@ $client->streamProtocolNoCollision(
-
$client->streamProtocolCollision($request) +
$client->streamProtocolCollision($request) -> SseStream
@@ -111,7 +111,7 @@ $client->streamProtocolCollision(
-
$client->streamDataContext($request) +
$client->streamDataContext($request) -> SseStream
@@ -167,7 +167,7 @@ $client->streamDataContext(
-
$client->streamNoContext($request) +
$client->streamNoContext($request) -> SseStream
@@ -223,7 +223,7 @@ $client->streamNoContext(
-
$client->streamProtocolWithFlatSchema($request) +
$client->streamProtocolWithFlatSchema($request) -> SseStream
@@ -279,7 +279,7 @@ $client->streamProtocolWithFlatSchema(
-
$client->streamDataContextWithEnvelopeSchema($request) +
$client->streamDataContextWithEnvelopeSchema($request) -> SseStream
@@ -335,7 +335,7 @@ $client->streamDataContextWithEnvelopeSchema(
-
$client->streamOasSpecNative($request) +
$client->streamOasSpecNative($request) -> SseStream
@@ -391,7 +391,7 @@ $client->streamOasSpecNative(
-
$client->streamXFernStreamingConditionStream($request) +
$client->streamXFernStreamingConditionStream($request) -> JsonStream
@@ -525,7 +525,7 @@ $client->streamXFernStreamingConditionStream(
-
$client->streamXFernStreamingSharedSchemaStream($request) +
$client->streamXFernStreamingSharedSchemaStream($request) -> JsonStream
@@ -752,7 +752,7 @@ $client->validateCompletion(
-
$client->streamXFernStreamingUnionStream($request) +
$client->streamXFernStreamingUnionStream($request) -> JsonStream
@@ -930,7 +930,7 @@ $client->validateUnionRequest(
-
$client->streamXFernStreamingNullableConditionStream($request) +
$client->streamXFernStreamingNullableConditionStream($request) -> JsonStream
@@ -1064,7 +1064,7 @@ $client->streamXFernStreamingNullableConditionStream(
-
$client->streamXFernStreamingSseOnly($request) +
$client->streamXFernStreamingSseOnly($request) -> SseStream
diff --git a/seed/php-sdk/server-sent-events-openapi/src/Core/Client/JsonStream.php b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/JsonStream.php new file mode 100644 index 000000000000..d5325e9f6a45 --- /dev/null +++ b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/JsonStream.php @@ -0,0 +1,39 @@ + + */ +class JsonStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per line with the raw + * JSON payload string. + * @param ?string $terminator Optional sentinel line that ends the stream + * when received. Pass `null` to read until EOF. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = null, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Json, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/server-sent-events-openapi/src/Core/Client/SseEvent.php b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/SseEvent.php new file mode 100644 index 000000000000..61ff24ba4aa7 --- /dev/null +++ b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/SseEvent.php @@ -0,0 +1,32 @@ + + */ +class SseStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per dispatched event + * with the raw `data:` payload string (newline-joined for multi-line frames). + * @param ?string $terminator Optional sentinel payload that ends the stream + * when received. Defaults to '[DONE]', a common SSE convention. + * Pass `null` to disable terminator handling. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = '[DONE]', + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + self::validateContentType($response); + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Sse, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } + + /** + * Iterates the stream yielding both the deserialized payload and the + * accompanying SSE metadata (event type, id, retry). Use this when you + * need the event field (e.g. for event-typed unions) or `Last-Event-ID` + * for resumption logic. + * + * For data-only iteration, use this object directly as an iterable: + * `foreach ($stream as $event) { ... }`. + * + * @return Generator> + */ + public function events(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield new SseEvent( + data: $this->deserialize($raw['data']), + event: $raw['event'], + id: $raw['id'], + retry: $raw['retry'], + ); + } + } + + /** + * Validates that the response's Content-Type matches an SSE stream. + * + * Per WHATWG, the SSE wire format is always UTF-8; we reject explicit + * non-UTF-8 charset parameters rather than risk silent mojibake. A missing + * Content-Type header is tolerated — some servers omit it on streaming + * responses — but a wrong media type or wrong charset always throws. + */ + private static function validateContentType(ResponseInterface $response): void + { + $contentType = $response->getHeaderLine('Content-Type'); + if ($contentType === '') { + return; + } + $parts = explode(';', $contentType); + $mediaType = strtolower(trim($parts[0])); + if ($mediaType !== 'text/event-stream') { + throw new RuntimeException( + "Expected Content-Type 'text/event-stream' for SSE response, got '{$mediaType}'", + ); + } + foreach (array_slice($parts, 1) as $param) { + $param = trim($param); + if (stripos($param, 'charset=') !== 0) { + continue; + } + $charset = strtolower(trim(substr($param, 8), " \"'")); + if ($charset !== '' && $charset !== 'utf-8' && $charset !== 'utf8') { + throw new RuntimeException( + "Unsupported SSE charset '{$charset}'; per the WHATWG spec only UTF-8 is permitted", + ); + } + } + } +} diff --git a/seed/php-sdk/server-sent-events-openapi/src/Core/Client/Stream.php b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/Stream.php new file mode 100644 index 000000000000..f7616c33b07e --- /dev/null +++ b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/Stream.php @@ -0,0 +1,287 @@ + + */ +class Stream implements IteratorAggregate +{ + public const DEFAULT_MAX_BUFFER_SIZE = 1_048_576; + + private const READ_CHUNK_SIZE = 8192; + private const UTF8_BOM = "\xEF\xBB\xBF"; + + private StreamInterface $body; + + /** @var Closure(string): T */ + private Closure $deserializer; + + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per frame with the raw payload string. + * For text streams, the deserializer is typically `fn(string $line) => $line`. + * @param StreamFormat $format Framing strategy for the stream. + * @param ?string $terminator Optional sentinel value that ends the stream when matched + * against the raw frame payload (e.g. '[DONE]'). + * @param int $maxBufferSize Maximum size in bytes for the line buffer or a single SSE + * event's accumulated `data` field. Exceeding this throws `RuntimeException` to + * guard against pathological streams. Defaults to 1 MiB. + */ + protected function __construct( + ResponseInterface $response, + Closure $deserializer, + private readonly StreamFormat $format = StreamFormat::Sse, + private readonly ?string $terminator = null, + private readonly int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + $this->body = $response->getBody(); + $this->deserializer = $deserializer; + } + + /** + * Iteration is one-shot: PSR-7 bodies are forward-only, so re-iterating the + * same `Stream` instance yields nothing useful. + * + * @return Generator + */ + public function getIterator(): Generator + { + return match ($this->format) { + StreamFormat::Sse => $this->iterateSse(), + StreamFormat::Json => $this->iterateDelimited(), + StreamFormat::Text => $this->iterateText(), + }; + } + + /** + * Applies the configured deserializer to a single raw frame payload. + * Available to subclasses (notably `SseStream::events()`) so they can + * construct typed envelopes without accessing the closure directly. + * + * @return T + */ + protected function deserialize(string $raw): mixed + { + return ($this->deserializer)($raw); + } + + /** + * @return Generator + */ + private function iterateSse(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield ($this->deserializer)($raw['data']); + } + } + + /** + * Iterates the SSE stream yielding raw envelopes with WHATWG metadata fields + * intact. Yields plain associative arrays — not `SseEvent` objects — so the + * data-only iteration path doesn't pay the allocation cost; `SseStream::events()` + * constructs the public `SseEvent` on top. + * + * Per WHATWG: the `id:` field persists across events within this iteration; + * the configured `terminator`, if present, ends iteration when matched against + * `data`. + * + * @internal + * @return Generator + */ + protected function iterateRawSseEvents(): Generator + { + $dataBuffer = ''; + $eventType = ''; + $lastEventId = ''; + $retry = null; + foreach ($this->readLines() as $line) { + if ($line === '') { + if ($dataBuffer === '') { + continue; + } + $payload = substr($dataBuffer, 0, -1); + $dataBuffer = ''; + if ($this->terminator !== null && $payload === $this->terminator) { + return; + } + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + // Per WHATWG: do NOT reset lastEventId between events. + $eventType = ''; + $retry = null; + continue; + } + if (str_starts_with($line, ':')) { + continue; + } + $colonPos = strpos($line, ':'); + if ($colonPos === false) { + if ($line === 'data') { + $dataBuffer = $this->appendWithinCap($dataBuffer, "\n"); + } + continue; + } + $field = substr($line, 0, $colonPos); + $value = substr($line, $colonPos + 1); + if (str_starts_with($value, ' ')) { + $value = substr($value, 1); + } + switch ($field) { + case 'data': + $dataBuffer = $this->appendWithinCap($dataBuffer, $value . "\n"); + break; + case 'event': + $eventType = $value; + break; + case 'id': + // WHATWG: ignore IDs that contain a NULL byte. + if (!str_contains($value, "\0")) { + $lastEventId = $value; + } + break; + case 'retry': + // WHATWG: ignore the value if it isn't a base-10 integer. + if ($value !== '' && ctype_digit($value)) { + $retry = (int) $value; + } + break; + } + } + // Flush a trailing event that lacked a closing blank line. + if ($dataBuffer !== '') { + $payload = substr($dataBuffer, 0, -1); + if ($this->terminator === null || $payload !== $this->terminator) { + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + } + } + } + + /** + * @return Generator + */ + private function iterateDelimited(): Generator + { + foreach ($this->readLines() as $line) { + if ($line === '') { + continue; + } + if ($this->terminator !== null && $line === $this->terminator) { + return; + } + yield ($this->deserializer)($line); + } + } + + /** + * @return Generator + */ + private function iterateText(): Generator + { + foreach ($this->readLines() as $line) { + yield ($this->deserializer)($line); + } + } + + /** + * Reads the response body and yields complete lines, normalizing CRLF/CR + * to LF per the WHATWG SSE spec. Strips a single UTF-8 BOM if present at + * the start of the stream (WHATWG §9.2.4). Trailing partial content + * (without a terminating newline) is emitted as a final line. + * + * `$pendingCr` tracks whether the prior chunk's last byte was `\r`, so a + * `\r\n` sequence split across a read boundary collapses to one terminator + * instead of two. + * + * @return Generator + */ + private function readLines(): Generator + { + $buffer = ''; + $pendingCr = false; + $bomChecked = false; + while (!$this->body->eof()) { + $chunk = $this->body->read(self::READ_CHUNK_SIZE); + if ($chunk === '') { + continue; + } + if ($pendingCr && $chunk[0] === "\n") { + $chunk = substr($chunk, 1); + } + if (str_contains($chunk, "\r")) { + $pendingCr = str_ends_with($chunk, "\r"); + $chunk = str_replace(["\r\n", "\r"], "\n", $chunk); + } else { + $pendingCr = false; + } + $buffer = $this->appendWithinCap($buffer, $chunk); + if (!$bomChecked && strlen($buffer) >= 3) { + $bomChecked = true; + if (str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + } + while (($lfPos = strpos($buffer, "\n")) !== false) { + yield substr($buffer, 0, $lfPos); + $buffer = substr($buffer, $lfPos + 1); + } + } + // BOM may not have been checked yet if the entire body was < 3 bytes. + if (!$bomChecked && str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + if ($buffer !== '') { + yield $buffer; + } + } + + /** + * Appends $suffix to $buffer, throwing `RuntimeException` if the resulting + * size would exceed `maxBufferSize`. + */ + private function appendWithinCap(string $buffer, string $suffix): string + { + if (strlen($buffer) + strlen($suffix) > $this->maxBufferSize) { + throw new RuntimeException( + "Stream buffer would exceed maximum size of {$this->maxBufferSize} bytes", + ); + } + return $buffer . $suffix; + } + + public function __destruct() + { + try { + $this->body->close(); + } catch (\Throwable) { + // Best effort — the body may already be closed by the consumer. + } + } +} diff --git a/seed/php-sdk/server-sent-events-openapi/src/Core/Client/StreamFormat.php b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/StreamFormat.php new file mode 100644 index 000000000000..2db924fefd94 --- /dev/null +++ b/seed/php-sdk/server-sent-events-openapi/src/Core/Client/StreamFormat.php @@ -0,0 +1,13 @@ + + */ +class TextStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: fn (string $line): string => $line, + format: StreamFormat::Text, + terminator: null, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/server-sent-events-openapi/src/SeedClient.php b/seed/php-sdk/server-sent-events-openapi/src/SeedClient.php index c5a0d68e6322..1cd42555e5d4 100644 --- a/seed/php-sdk/server-sent-events-openapi/src/SeedClient.php +++ b/seed/php-sdk/server-sent-events-openapi/src/SeedClient.php @@ -5,12 +5,22 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; use Seed\Types\StreamRequest; +use Seed\Core\Client\SseStream; +use Seed\Types\StreamProtocolNoCollisionResponse; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; use Seed\Core\Client\HttpMethod; use Psr\Http\Client\ClientExceptionInterface; +use Seed\Types\StreamProtocolCollisionResponse; +use Seed\Types\StreamDataContextResponse; +use Seed\Types\StreamNoContextResponse; +use Seed\Types\StreamProtocolWithFlatSchemaResponse; +use Seed\Types\StreamDataContextWithEnvelopeSchemaResponse; +use Seed\Types\Event; use Seed\Requests\StreamXFernStreamingConditionStreamRequest; +use Seed\Core\Client\JsonStream; +use Seed\Types\CompletionStreamChunk; use Seed\Requests\StreamXFernStreamingConditionRequest; use Seed\Types\CompletionFullResponse; use JsonException; @@ -23,6 +33,7 @@ use Seed\Types\ValidateUnionRequestResponse; use Seed\Requests\StreamXFernStreamingNullableConditionStreamRequest; use Seed\Requests\StreamXFernStreamingNullableConditionRequest; +use Seed\Core\Json\JsonDecoder; class SeedClient { @@ -85,10 +96,11 @@ public function __construct( * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamProtocolNoCollision(StreamRequest $request, ?array $options = null): void + public function streamProtocolNoCollision(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -102,6 +114,9 @@ public function streamProtocolNoCollision(StreamRequest $request, ?array $option $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamProtocolNoCollisionResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -124,10 +139,11 @@ public function streamProtocolNoCollision(StreamRequest $request, ?array $option * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamProtocolCollision(StreamRequest $request, ?array $options = null): void + public function streamProtocolCollision(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -141,6 +157,9 @@ public function streamProtocolCollision(StreamRequest $request, ?array $options $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamProtocolCollisionResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -163,10 +182,11 @@ public function streamProtocolCollision(StreamRequest $request, ?array $options * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamDataContext(StreamRequest $request, ?array $options = null): void + public function streamDataContext(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -180,6 +200,9 @@ public function streamDataContext(StreamRequest $request, ?array $options = null $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamDataContextResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -202,10 +225,11 @@ public function streamDataContext(StreamRequest $request, ?array $options = null * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamNoContext(StreamRequest $request, ?array $options = null): void + public function streamNoContext(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -219,6 +243,9 @@ public function streamNoContext(StreamRequest $request, ?array $options = null): $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamNoContextResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -241,10 +268,11 @@ public function streamNoContext(StreamRequest $request, ?array $options = null): * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamProtocolWithFlatSchema(StreamRequest $request, ?array $options = null): void + public function streamProtocolWithFlatSchema(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -258,6 +286,9 @@ public function streamProtocolWithFlatSchema(StreamRequest $request, ?array $opt $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamProtocolWithFlatSchemaResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -280,10 +311,11 @@ public function streamProtocolWithFlatSchema(StreamRequest $request, ?array $opt * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamDataContextWithEnvelopeSchema(StreamRequest $request, ?array $options = null): void + public function streamDataContextWithEnvelopeSchema(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -297,6 +329,9 @@ public function streamDataContextWithEnvelopeSchema(StreamRequest $request, ?arr $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamDataContextWithEnvelopeSchemaResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -319,10 +354,11 @@ public function streamDataContextWithEnvelopeSchema(StreamRequest $request, ?arr * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamOasSpecNative(StreamRequest $request, ?array $options = null): void + public function streamOasSpecNative(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -336,6 +372,9 @@ public function streamOasSpecNative(StreamRequest $request, ?array $options = nu $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => Event::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -358,10 +397,11 @@ public function streamOasSpecNative(StreamRequest $request, ?array $options = nu * queryParameters?: array, * bodyProperties?: array, * } $options + * @return JsonStream * @throws SeedException * @throws SeedApiException */ - public function streamXFernStreamingConditionStream(StreamXFernStreamingConditionStreamRequest $request, ?array $options = null): void + public function streamXFernStreamingConditionStream(StreamXFernStreamingConditionStreamRequest $request, ?array $options = null): JsonStream { $options = array_merge($this->options, $options ?? []); try { @@ -375,6 +415,9 @@ public function streamXFernStreamingConditionStream(StreamXFernStreamingConditio $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new JsonStream(response: $response, deserializer: fn (string $data) => CompletionStreamChunk::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -446,10 +489,11 @@ public function streamXFernStreamingCondition(StreamXFernStreamingConditionReque * queryParameters?: array, * bodyProperties?: array, * } $options + * @return JsonStream * @throws SeedException * @throws SeedApiException */ - public function streamXFernStreamingSharedSchemaStream(StreamXFernStreamingSharedSchemaStreamRequest $request, ?array $options = null): void + public function streamXFernStreamingSharedSchemaStream(StreamXFernStreamingSharedSchemaStreamRequest $request, ?array $options = null): JsonStream { $options = array_merge($this->options, $options ?? []); try { @@ -463,6 +507,9 @@ public function streamXFernStreamingSharedSchemaStream(StreamXFernStreamingShare $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new JsonStream(response: $response, deserializer: fn (string $data) => CompletionStreamChunk::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -583,10 +630,11 @@ public function validateCompletion(SharedCompletionRequest $request, ?array $opt * queryParameters?: array, * bodyProperties?: array, * } $options + * @return JsonStream * @throws SeedException * @throws SeedApiException */ - public function streamXFernStreamingUnionStream(StreamXFernStreamingUnionStreamRequest $request, ?array $options = null): void + public function streamXFernStreamingUnionStream(StreamXFernStreamingUnionStreamRequest $request, ?array $options = null): JsonStream { $options = array_merge($this->options, $options ?? []); try { @@ -600,6 +648,9 @@ public function streamXFernStreamingUnionStream(StreamXFernStreamingUnionStreamR $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new JsonStream(response: $response, deserializer: fn (string $data) => CompletionStreamChunk::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -720,10 +771,11 @@ public function validateUnionRequest(UnionStreamRequestBase $request, ?array $op * queryParameters?: array, * bodyProperties?: array, * } $options + * @return JsonStream * @throws SeedException * @throws SeedApiException */ - public function streamXFernStreamingNullableConditionStream(StreamXFernStreamingNullableConditionStreamRequest $request, ?array $options = null): void + public function streamXFernStreamingNullableConditionStream(StreamXFernStreamingNullableConditionStreamRequest $request, ?array $options = null): JsonStream { $options = array_merge($this->options, $options ?? []); try { @@ -737,6 +789,9 @@ public function streamXFernStreamingNullableConditionStream(StreamXFernStreaming $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new JsonStream(response: $response, deserializer: fn (string $data) => CompletionStreamChunk::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -808,10 +863,11 @@ public function streamXFernStreamingNullableCondition(StreamXFernStreamingNullab * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamXFernStreamingSseOnly(StreamRequest $request, ?array $options = null): void + public function streamXFernStreamingSseOnly(StreamRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -825,6 +881,9 @@ public function streamXFernStreamingSseOnly(StreamRequest $request, ?array $opti $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => JsonDecoder::decodeString($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } diff --git a/seed/php-sdk/server-sent-events-openapi/tests/Core/Client/StreamTest.php b/seed/php-sdk/server-sent-events-openapi/tests/Core/Client/StreamTest.php new file mode 100644 index 000000000000..ed6ae35d9119 --- /dev/null +++ b/seed/php-sdk/server-sent-events-openapi/tests/Core/Client/StreamTest.php @@ -0,0 +1,297 @@ + $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseConcatenatesMultilineDataWithNewlines(): void + { + $body = "data: line one\ndata: line two\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["line one\nline two"], iterator_to_array($stream, false)); + } + + public function testSseStripsLeadingSpaceFromFieldValues(): void + { + // SSE spec: a single leading space in the field value is stripped. + $body = "data: with-leading-space\ndata:no-leading-space\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["with-leading-space\nno-leading-space"], iterator_to_array($stream, false)); + } + + public function testSseIgnoresCommentLines(): void + { + $body = ": this is a comment\ndata: payload\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['payload'], iterator_to_array($stream, false)); + } + + public function testSseTerminatorEndsIterationCleanly(): void + { + $body = "data: first\n\ndata: [DONE]\n\ndata: never-yielded\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, '[DONE]'); + + $this->assertSame(['first'], iterator_to_array($stream, false)); + } + + public function testSseNormalizesCrlfAndLoneCr(): void + { + $body = "data: a\r\n\r\ndata: b\rdata: c\r\r"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['a', "b\nc"], iterator_to_array($stream, false)); + } + + public function testSseDispatchesTrailingEventWithoutBlankLine(): void + { + $body = "data: incomplete"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['incomplete'], iterator_to_array($stream, false)); + } + + public function testSseAppliesDeserializerOncePerEvent(): void + { + $body = "data: {\"n\":1}\n\ndata: {\"n\":2}\n\n"; + $stream = new SseStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2]], iterator_to_array($stream, false)); + } + + public function testSseEventsExposesEventIdAndRetryMetadata(): void + { + $body = "event: chat\nid: msg-1\nretry: 5000\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertCount(1, $events); + $this->assertInstanceOf(SseEvent::class, $events[0]); + $this->assertSame('hi', $events[0]->data); + $this->assertSame('chat', $events[0]->event); + $this->assertSame('msg-1', $events[0]->id); + $this->assertSame(5000, $events[0]->retry); + } + + public function testSseEventsPersistsLastEventIdAcrossEventsPerSpec(): void + { + // Per WHATWG SSE: once an `id:` is set it persists across subsequent + // events until explicitly overridden. + $body = "id: persistent\ndata: a\n\ndata: b\n\nid: replaced\ndata: c\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['persistent', 'persistent', 'replaced'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresIdContainingNullByte(): void + { + $body = "id: ok\ndata: first\n\nid: bad\0id\ndata: second\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['ok', 'ok'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresNonIntegerRetry(): void + { + $body = "retry: not-a-number\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertNull($events[0]->retry); + } + + public function testSseConstructorRejectsNonSseContentType(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/text\/event-stream/'); + + new SseStream( + self::response('data: x\n\n', contentType: 'application/json'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorRejectsNonUtf8Charset(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/charset/i'); + + new SseStream( + self::response('data: x\n\n', contentType: 'text/event-stream; charset=iso-8859-1'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorAcceptsUtf8CharsetParameter(): void + { + $stream = new SseStream( + self::response("data: hi\n\n", contentType: 'text/event-stream; charset=UTF-8'), + fn (string $d): string => $d, + null, + ); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testSseConstructorToleratesMissingContentTypeHeader(): void + { + $response = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withBody(\Http\Discovery\Psr17FactoryDiscovery::findStreamFactory()->createStream("data: hi\n\n")); + + $stream = new SseStream($response, fn (string $d): string => $d, null); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testStreamThrowsWhenLineBufferExceedsMaxSize(): void + { + $bigLine = str_repeat('A', 200) . "\n"; + $stream = new SseStream( + self::response("data: " . $bigLine . "\n"), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testStreamThrowsBeforeAccumulatingPastMaxBufferOnLongRunningSseEvent(): void + { + // Each line is well under the 64-byte cap; the cumulative `data:` + // append must trip the check before the buffer balloons. + $manyDataLines = ''; + for ($i = 0; $i < 50; $i++) { + $manyDataLines .= "data: chunk-$i\n"; + } + $stream = new SseStream( + self::response($manyDataLines), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testSseStripsUtf8BomFromStartOfStream(): void + { + // WHATWG §9.2.4 requires a leading U+FEFF to be dropped. + $body = "\xEF\xBB\xBFdata: hello\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseStripsBomOnlyAtVeryStart(): void + { + $body = "data: first\n\ndata: \xEF\xBB\xBFsecond\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['first', "\xEF\xBB\xBFsecond"], iterator_to_array($stream, false)); + } + + public function testJsonStreamYieldsOnePerLine(): void + { + $body = "{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2], ['n' => 3]], iterator_to_array($stream, false)); + } + + public function testJsonStreamSkipsEmptyLines(): void + { + $body = "{\"a\":1}\n\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['a' => 1], ['a' => 2]], iterator_to_array($stream, false)); + } + + public function testJsonStreamTerminatorEndsIteration(): void + { + $body = "{\"a\":1}\n[DONE]\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), '[DONE]'); + + $this->assertSame([['a' => 1]], iterator_to_array($stream, false)); + } + + /** + * @return \Closure(string): array + */ + private static function jsonDecoder(): \Closure + { + return static function (string $raw): array { + /** @var array $decoded */ + $decoded = json_decode($raw, true); + return $decoded; + }; + } + + public function testTextStreamYieldsRawLines(): void + { + $body = "alpha\nbeta\ngamma\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', 'beta', 'gamma'], iterator_to_array($stream, false)); + } + + public function testTextStreamPreservesEmptyLines(): void + { + $body = "alpha\n\nbeta\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', '', 'beta'], iterator_to_array($stream, false)); + } + + /** + * Build a PSR-7 ResponseInterface with the given body. The Content-Type + * defaults to `text/event-stream` so SSE tests just work; pass an override + * for the validation-error cases. + */ + private static function response( + string $body, + string $contentType = 'text/event-stream', + ): ResponseInterface { + return \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withHeader('Content-Type', $contentType) + ->withBody( + \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory() + ->createStream($body), + ); + } +} diff --git a/seed/php-sdk/server-sent-events/reference.md b/seed/php-sdk/server-sent-events/reference.md index 2da6a1938d9c..043b81f145c4 100644 --- a/seed/php-sdk/server-sent-events/reference.md +++ b/seed/php-sdk/server-sent-events/reference.md @@ -1,6 +1,6 @@ # Reference ## Completions -
$client->completions->stream($request) +
$client->completions->stream($request) -> SseStream
@@ -44,7 +44,7 @@ $client->completions->stream(
-
$client->completions->streamWithoutTerminator($request) +
$client->completions->streamWithoutTerminator($request) -> SseStream
diff --git a/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php b/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php index b900c289b51b..9acb2de19eb5 100644 --- a/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php +++ b/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php @@ -5,6 +5,8 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; use Seed\Completions\Requests\StreamCompletionRequest; +use Seed\Core\Client\SseStream; +use Seed\Completions\Types\StreamedCompletion; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; @@ -58,10 +60,11 @@ public function __construct( * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function stream(StreamCompletionRequest $request, ?array $options = null): void + public function stream(StreamCompletionRequest $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -75,6 +78,9 @@ public function stream(StreamCompletionRequest $request, ?array $options = null) $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamedCompletion::fromJson($data), terminator: '[[DONE]]'); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } @@ -95,10 +101,11 @@ public function stream(StreamCompletionRequest $request, ?array $options = null) * queryParameters?: array, * bodyProperties?: array, * } $options + * @return SseStream * @throws SeedException * @throws SeedApiException */ - public function streamWithoutTerminator(StreamCompletionRequestWithoutTerminator $request, ?array $options = null): void + public function streamWithoutTerminator(StreamCompletionRequestWithoutTerminator $request, ?array $options = null): SseStream { $options = array_merge($this->options, $options ?? []); try { @@ -112,6 +119,9 @@ public function streamWithoutTerminator(StreamCompletionRequestWithoutTerminator $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new SseStream(response: $response, deserializer: fn (string $data) => StreamedCompletion::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } diff --git a/seed/php-sdk/server-sent-events/src/Core/Client/JsonStream.php b/seed/php-sdk/server-sent-events/src/Core/Client/JsonStream.php new file mode 100644 index 000000000000..d5325e9f6a45 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Client/JsonStream.php @@ -0,0 +1,39 @@ + + */ +class JsonStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per line with the raw + * JSON payload string. + * @param ?string $terminator Optional sentinel line that ends the stream + * when received. Pass `null` to read until EOF. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = null, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Json, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/Client/SseEvent.php b/seed/php-sdk/server-sent-events/src/Core/Client/SseEvent.php new file mode 100644 index 000000000000..61ff24ba4aa7 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Client/SseEvent.php @@ -0,0 +1,32 @@ + + */ +class SseStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per dispatched event + * with the raw `data:` payload string (newline-joined for multi-line frames). + * @param ?string $terminator Optional sentinel payload that ends the stream + * when received. Defaults to '[DONE]', a common SSE convention. + * Pass `null` to disable terminator handling. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = '[DONE]', + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + self::validateContentType($response); + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Sse, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } + + /** + * Iterates the stream yielding both the deserialized payload and the + * accompanying SSE metadata (event type, id, retry). Use this when you + * need the event field (e.g. for event-typed unions) or `Last-Event-ID` + * for resumption logic. + * + * For data-only iteration, use this object directly as an iterable: + * `foreach ($stream as $event) { ... }`. + * + * @return Generator> + */ + public function events(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield new SseEvent( + data: $this->deserialize($raw['data']), + event: $raw['event'], + id: $raw['id'], + retry: $raw['retry'], + ); + } + } + + /** + * Validates that the response's Content-Type matches an SSE stream. + * + * Per WHATWG, the SSE wire format is always UTF-8; we reject explicit + * non-UTF-8 charset parameters rather than risk silent mojibake. A missing + * Content-Type header is tolerated — some servers omit it on streaming + * responses — but a wrong media type or wrong charset always throws. + */ + private static function validateContentType(ResponseInterface $response): void + { + $contentType = $response->getHeaderLine('Content-Type'); + if ($contentType === '') { + return; + } + $parts = explode(';', $contentType); + $mediaType = strtolower(trim($parts[0])); + if ($mediaType !== 'text/event-stream') { + throw new RuntimeException( + "Expected Content-Type 'text/event-stream' for SSE response, got '{$mediaType}'", + ); + } + foreach (array_slice($parts, 1) as $param) { + $param = trim($param); + if (stripos($param, 'charset=') !== 0) { + continue; + } + $charset = strtolower(trim(substr($param, 8), " \"'")); + if ($charset !== '' && $charset !== 'utf-8' && $charset !== 'utf8') { + throw new RuntimeException( + "Unsupported SSE charset '{$charset}'; per the WHATWG spec only UTF-8 is permitted", + ); + } + } + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/Client/Stream.php b/seed/php-sdk/server-sent-events/src/Core/Client/Stream.php new file mode 100644 index 000000000000..f7616c33b07e --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Client/Stream.php @@ -0,0 +1,287 @@ + + */ +class Stream implements IteratorAggregate +{ + public const DEFAULT_MAX_BUFFER_SIZE = 1_048_576; + + private const READ_CHUNK_SIZE = 8192; + private const UTF8_BOM = "\xEF\xBB\xBF"; + + private StreamInterface $body; + + /** @var Closure(string): T */ + private Closure $deserializer; + + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per frame with the raw payload string. + * For text streams, the deserializer is typically `fn(string $line) => $line`. + * @param StreamFormat $format Framing strategy for the stream. + * @param ?string $terminator Optional sentinel value that ends the stream when matched + * against the raw frame payload (e.g. '[DONE]'). + * @param int $maxBufferSize Maximum size in bytes for the line buffer or a single SSE + * event's accumulated `data` field. Exceeding this throws `RuntimeException` to + * guard against pathological streams. Defaults to 1 MiB. + */ + protected function __construct( + ResponseInterface $response, + Closure $deserializer, + private readonly StreamFormat $format = StreamFormat::Sse, + private readonly ?string $terminator = null, + private readonly int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + $this->body = $response->getBody(); + $this->deserializer = $deserializer; + } + + /** + * Iteration is one-shot: PSR-7 bodies are forward-only, so re-iterating the + * same `Stream` instance yields nothing useful. + * + * @return Generator + */ + public function getIterator(): Generator + { + return match ($this->format) { + StreamFormat::Sse => $this->iterateSse(), + StreamFormat::Json => $this->iterateDelimited(), + StreamFormat::Text => $this->iterateText(), + }; + } + + /** + * Applies the configured deserializer to a single raw frame payload. + * Available to subclasses (notably `SseStream::events()`) so they can + * construct typed envelopes without accessing the closure directly. + * + * @return T + */ + protected function deserialize(string $raw): mixed + { + return ($this->deserializer)($raw); + } + + /** + * @return Generator + */ + private function iterateSse(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield ($this->deserializer)($raw['data']); + } + } + + /** + * Iterates the SSE stream yielding raw envelopes with WHATWG metadata fields + * intact. Yields plain associative arrays — not `SseEvent` objects — so the + * data-only iteration path doesn't pay the allocation cost; `SseStream::events()` + * constructs the public `SseEvent` on top. + * + * Per WHATWG: the `id:` field persists across events within this iteration; + * the configured `terminator`, if present, ends iteration when matched against + * `data`. + * + * @internal + * @return Generator + */ + protected function iterateRawSseEvents(): Generator + { + $dataBuffer = ''; + $eventType = ''; + $lastEventId = ''; + $retry = null; + foreach ($this->readLines() as $line) { + if ($line === '') { + if ($dataBuffer === '') { + continue; + } + $payload = substr($dataBuffer, 0, -1); + $dataBuffer = ''; + if ($this->terminator !== null && $payload === $this->terminator) { + return; + } + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + // Per WHATWG: do NOT reset lastEventId between events. + $eventType = ''; + $retry = null; + continue; + } + if (str_starts_with($line, ':')) { + continue; + } + $colonPos = strpos($line, ':'); + if ($colonPos === false) { + if ($line === 'data') { + $dataBuffer = $this->appendWithinCap($dataBuffer, "\n"); + } + continue; + } + $field = substr($line, 0, $colonPos); + $value = substr($line, $colonPos + 1); + if (str_starts_with($value, ' ')) { + $value = substr($value, 1); + } + switch ($field) { + case 'data': + $dataBuffer = $this->appendWithinCap($dataBuffer, $value . "\n"); + break; + case 'event': + $eventType = $value; + break; + case 'id': + // WHATWG: ignore IDs that contain a NULL byte. + if (!str_contains($value, "\0")) { + $lastEventId = $value; + } + break; + case 'retry': + // WHATWG: ignore the value if it isn't a base-10 integer. + if ($value !== '' && ctype_digit($value)) { + $retry = (int) $value; + } + break; + } + } + // Flush a trailing event that lacked a closing blank line. + if ($dataBuffer !== '') { + $payload = substr($dataBuffer, 0, -1); + if ($this->terminator === null || $payload !== $this->terminator) { + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + } + } + } + + /** + * @return Generator + */ + private function iterateDelimited(): Generator + { + foreach ($this->readLines() as $line) { + if ($line === '') { + continue; + } + if ($this->terminator !== null && $line === $this->terminator) { + return; + } + yield ($this->deserializer)($line); + } + } + + /** + * @return Generator + */ + private function iterateText(): Generator + { + foreach ($this->readLines() as $line) { + yield ($this->deserializer)($line); + } + } + + /** + * Reads the response body and yields complete lines, normalizing CRLF/CR + * to LF per the WHATWG SSE spec. Strips a single UTF-8 BOM if present at + * the start of the stream (WHATWG §9.2.4). Trailing partial content + * (without a terminating newline) is emitted as a final line. + * + * `$pendingCr` tracks whether the prior chunk's last byte was `\r`, so a + * `\r\n` sequence split across a read boundary collapses to one terminator + * instead of two. + * + * @return Generator + */ + private function readLines(): Generator + { + $buffer = ''; + $pendingCr = false; + $bomChecked = false; + while (!$this->body->eof()) { + $chunk = $this->body->read(self::READ_CHUNK_SIZE); + if ($chunk === '') { + continue; + } + if ($pendingCr && $chunk[0] === "\n") { + $chunk = substr($chunk, 1); + } + if (str_contains($chunk, "\r")) { + $pendingCr = str_ends_with($chunk, "\r"); + $chunk = str_replace(["\r\n", "\r"], "\n", $chunk); + } else { + $pendingCr = false; + } + $buffer = $this->appendWithinCap($buffer, $chunk); + if (!$bomChecked && strlen($buffer) >= 3) { + $bomChecked = true; + if (str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + } + while (($lfPos = strpos($buffer, "\n")) !== false) { + yield substr($buffer, 0, $lfPos); + $buffer = substr($buffer, $lfPos + 1); + } + } + // BOM may not have been checked yet if the entire body was < 3 bytes. + if (!$bomChecked && str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + if ($buffer !== '') { + yield $buffer; + } + } + + /** + * Appends $suffix to $buffer, throwing `RuntimeException` if the resulting + * size would exceed `maxBufferSize`. + */ + private function appendWithinCap(string $buffer, string $suffix): string + { + if (strlen($buffer) + strlen($suffix) > $this->maxBufferSize) { + throw new RuntimeException( + "Stream buffer would exceed maximum size of {$this->maxBufferSize} bytes", + ); + } + return $buffer . $suffix; + } + + public function __destruct() + { + try { + $this->body->close(); + } catch (\Throwable) { + // Best effort — the body may already be closed by the consumer. + } + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/Client/StreamFormat.php b/seed/php-sdk/server-sent-events/src/Core/Client/StreamFormat.php new file mode 100644 index 000000000000..2db924fefd94 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Client/StreamFormat.php @@ -0,0 +1,13 @@ + + */ +class TextStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: fn (string $line): string => $line, + format: StreamFormat::Text, + terminator: null, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Core/Client/StreamTest.php b/seed/php-sdk/server-sent-events/tests/Core/Client/StreamTest.php new file mode 100644 index 000000000000..ed6ae35d9119 --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Core/Client/StreamTest.php @@ -0,0 +1,297 @@ + $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseConcatenatesMultilineDataWithNewlines(): void + { + $body = "data: line one\ndata: line two\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["line one\nline two"], iterator_to_array($stream, false)); + } + + public function testSseStripsLeadingSpaceFromFieldValues(): void + { + // SSE spec: a single leading space in the field value is stripped. + $body = "data: with-leading-space\ndata:no-leading-space\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["with-leading-space\nno-leading-space"], iterator_to_array($stream, false)); + } + + public function testSseIgnoresCommentLines(): void + { + $body = ": this is a comment\ndata: payload\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['payload'], iterator_to_array($stream, false)); + } + + public function testSseTerminatorEndsIterationCleanly(): void + { + $body = "data: first\n\ndata: [DONE]\n\ndata: never-yielded\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, '[DONE]'); + + $this->assertSame(['first'], iterator_to_array($stream, false)); + } + + public function testSseNormalizesCrlfAndLoneCr(): void + { + $body = "data: a\r\n\r\ndata: b\rdata: c\r\r"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['a', "b\nc"], iterator_to_array($stream, false)); + } + + public function testSseDispatchesTrailingEventWithoutBlankLine(): void + { + $body = "data: incomplete"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['incomplete'], iterator_to_array($stream, false)); + } + + public function testSseAppliesDeserializerOncePerEvent(): void + { + $body = "data: {\"n\":1}\n\ndata: {\"n\":2}\n\n"; + $stream = new SseStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2]], iterator_to_array($stream, false)); + } + + public function testSseEventsExposesEventIdAndRetryMetadata(): void + { + $body = "event: chat\nid: msg-1\nretry: 5000\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertCount(1, $events); + $this->assertInstanceOf(SseEvent::class, $events[0]); + $this->assertSame('hi', $events[0]->data); + $this->assertSame('chat', $events[0]->event); + $this->assertSame('msg-1', $events[0]->id); + $this->assertSame(5000, $events[0]->retry); + } + + public function testSseEventsPersistsLastEventIdAcrossEventsPerSpec(): void + { + // Per WHATWG SSE: once an `id:` is set it persists across subsequent + // events until explicitly overridden. + $body = "id: persistent\ndata: a\n\ndata: b\n\nid: replaced\ndata: c\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['persistent', 'persistent', 'replaced'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresIdContainingNullByte(): void + { + $body = "id: ok\ndata: first\n\nid: bad\0id\ndata: second\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['ok', 'ok'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresNonIntegerRetry(): void + { + $body = "retry: not-a-number\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertNull($events[0]->retry); + } + + public function testSseConstructorRejectsNonSseContentType(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/text\/event-stream/'); + + new SseStream( + self::response('data: x\n\n', contentType: 'application/json'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorRejectsNonUtf8Charset(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/charset/i'); + + new SseStream( + self::response('data: x\n\n', contentType: 'text/event-stream; charset=iso-8859-1'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorAcceptsUtf8CharsetParameter(): void + { + $stream = new SseStream( + self::response("data: hi\n\n", contentType: 'text/event-stream; charset=UTF-8'), + fn (string $d): string => $d, + null, + ); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testSseConstructorToleratesMissingContentTypeHeader(): void + { + $response = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withBody(\Http\Discovery\Psr17FactoryDiscovery::findStreamFactory()->createStream("data: hi\n\n")); + + $stream = new SseStream($response, fn (string $d): string => $d, null); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testStreamThrowsWhenLineBufferExceedsMaxSize(): void + { + $bigLine = str_repeat('A', 200) . "\n"; + $stream = new SseStream( + self::response("data: " . $bigLine . "\n"), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testStreamThrowsBeforeAccumulatingPastMaxBufferOnLongRunningSseEvent(): void + { + // Each line is well under the 64-byte cap; the cumulative `data:` + // append must trip the check before the buffer balloons. + $manyDataLines = ''; + for ($i = 0; $i < 50; $i++) { + $manyDataLines .= "data: chunk-$i\n"; + } + $stream = new SseStream( + self::response($manyDataLines), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testSseStripsUtf8BomFromStartOfStream(): void + { + // WHATWG §9.2.4 requires a leading U+FEFF to be dropped. + $body = "\xEF\xBB\xBFdata: hello\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseStripsBomOnlyAtVeryStart(): void + { + $body = "data: first\n\ndata: \xEF\xBB\xBFsecond\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['first', "\xEF\xBB\xBFsecond"], iterator_to_array($stream, false)); + } + + public function testJsonStreamYieldsOnePerLine(): void + { + $body = "{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2], ['n' => 3]], iterator_to_array($stream, false)); + } + + public function testJsonStreamSkipsEmptyLines(): void + { + $body = "{\"a\":1}\n\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['a' => 1], ['a' => 2]], iterator_to_array($stream, false)); + } + + public function testJsonStreamTerminatorEndsIteration(): void + { + $body = "{\"a\":1}\n[DONE]\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), '[DONE]'); + + $this->assertSame([['a' => 1]], iterator_to_array($stream, false)); + } + + /** + * @return \Closure(string): array + */ + private static function jsonDecoder(): \Closure + { + return static function (string $raw): array { + /** @var array $decoded */ + $decoded = json_decode($raw, true); + return $decoded; + }; + } + + public function testTextStreamYieldsRawLines(): void + { + $body = "alpha\nbeta\ngamma\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', 'beta', 'gamma'], iterator_to_array($stream, false)); + } + + public function testTextStreamPreservesEmptyLines(): void + { + $body = "alpha\n\nbeta\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', '', 'beta'], iterator_to_array($stream, false)); + } + + /** + * Build a PSR-7 ResponseInterface with the given body. The Content-Type + * defaults to `text/event-stream` so SSE tests just work; pass an override + * for the validation-error cases. + */ + private static function response( + string $body, + string $contentType = 'text/event-stream', + ): ResponseInterface { + return \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withHeader('Content-Type', $contentType) + ->withBody( + \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory() + ->createStream($body), + ); + } +} diff --git a/seed/php-sdk/streaming/reference.md b/seed/php-sdk/streaming/reference.md index 39d669640a34..71f5d8cac330 100644 --- a/seed/php-sdk/streaming/reference.md +++ b/seed/php-sdk/streaming/reference.md @@ -1,6 +1,6 @@ # Reference ## Dummy -
$client->dummy->generateStream($request) +
$client->dummy->generateStream($request) -> JsonStream
diff --git a/seed/php-sdk/streaming/src/Core/Client/JsonStream.php b/seed/php-sdk/streaming/src/Core/Client/JsonStream.php new file mode 100644 index 000000000000..d5325e9f6a45 --- /dev/null +++ b/seed/php-sdk/streaming/src/Core/Client/JsonStream.php @@ -0,0 +1,39 @@ + + */ +class JsonStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per line with the raw + * JSON payload string. + * @param ?string $terminator Optional sentinel line that ends the stream + * when received. Pass `null` to read until EOF. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = null, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Json, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/streaming/src/Core/Client/SseEvent.php b/seed/php-sdk/streaming/src/Core/Client/SseEvent.php new file mode 100644 index 000000000000..61ff24ba4aa7 --- /dev/null +++ b/seed/php-sdk/streaming/src/Core/Client/SseEvent.php @@ -0,0 +1,32 @@ + + */ +class SseStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per dispatched event + * with the raw `data:` payload string (newline-joined for multi-line frames). + * @param ?string $terminator Optional sentinel payload that ends the stream + * when received. Defaults to '[DONE]', a common SSE convention. + * Pass `null` to disable terminator handling. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + Closure $deserializer, + ?string $terminator = '[DONE]', + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + self::validateContentType($response); + parent::__construct( + response: $response, + deserializer: $deserializer, + format: StreamFormat::Sse, + terminator: $terminator, + maxBufferSize: $maxBufferSize, + ); + } + + /** + * Iterates the stream yielding both the deserialized payload and the + * accompanying SSE metadata (event type, id, retry). Use this when you + * need the event field (e.g. for event-typed unions) or `Last-Event-ID` + * for resumption logic. + * + * For data-only iteration, use this object directly as an iterable: + * `foreach ($stream as $event) { ... }`. + * + * @return Generator> + */ + public function events(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield new SseEvent( + data: $this->deserialize($raw['data']), + event: $raw['event'], + id: $raw['id'], + retry: $raw['retry'], + ); + } + } + + /** + * Validates that the response's Content-Type matches an SSE stream. + * + * Per WHATWG, the SSE wire format is always UTF-8; we reject explicit + * non-UTF-8 charset parameters rather than risk silent mojibake. A missing + * Content-Type header is tolerated — some servers omit it on streaming + * responses — but a wrong media type or wrong charset always throws. + */ + private static function validateContentType(ResponseInterface $response): void + { + $contentType = $response->getHeaderLine('Content-Type'); + if ($contentType === '') { + return; + } + $parts = explode(';', $contentType); + $mediaType = strtolower(trim($parts[0])); + if ($mediaType !== 'text/event-stream') { + throw new RuntimeException( + "Expected Content-Type 'text/event-stream' for SSE response, got '{$mediaType}'", + ); + } + foreach (array_slice($parts, 1) as $param) { + $param = trim($param); + if (stripos($param, 'charset=') !== 0) { + continue; + } + $charset = strtolower(trim(substr($param, 8), " \"'")); + if ($charset !== '' && $charset !== 'utf-8' && $charset !== 'utf8') { + throw new RuntimeException( + "Unsupported SSE charset '{$charset}'; per the WHATWG spec only UTF-8 is permitted", + ); + } + } + } +} diff --git a/seed/php-sdk/streaming/src/Core/Client/Stream.php b/seed/php-sdk/streaming/src/Core/Client/Stream.php new file mode 100644 index 000000000000..f7616c33b07e --- /dev/null +++ b/seed/php-sdk/streaming/src/Core/Client/Stream.php @@ -0,0 +1,287 @@ + + */ +class Stream implements IteratorAggregate +{ + public const DEFAULT_MAX_BUFFER_SIZE = 1_048_576; + + private const READ_CHUNK_SIZE = 8192; + private const UTF8_BOM = "\xEF\xBB\xBF"; + + private StreamInterface $body; + + /** @var Closure(string): T */ + private Closure $deserializer; + + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param Closure(string): T $deserializer Called once per frame with the raw payload string. + * For text streams, the deserializer is typically `fn(string $line) => $line`. + * @param StreamFormat $format Framing strategy for the stream. + * @param ?string $terminator Optional sentinel value that ends the stream when matched + * against the raw frame payload (e.g. '[DONE]'). + * @param int $maxBufferSize Maximum size in bytes for the line buffer or a single SSE + * event's accumulated `data` field. Exceeding this throws `RuntimeException` to + * guard against pathological streams. Defaults to 1 MiB. + */ + protected function __construct( + ResponseInterface $response, + Closure $deserializer, + private readonly StreamFormat $format = StreamFormat::Sse, + private readonly ?string $terminator = null, + private readonly int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + $this->body = $response->getBody(); + $this->deserializer = $deserializer; + } + + /** + * Iteration is one-shot: PSR-7 bodies are forward-only, so re-iterating the + * same `Stream` instance yields nothing useful. + * + * @return Generator + */ + public function getIterator(): Generator + { + return match ($this->format) { + StreamFormat::Sse => $this->iterateSse(), + StreamFormat::Json => $this->iterateDelimited(), + StreamFormat::Text => $this->iterateText(), + }; + } + + /** + * Applies the configured deserializer to a single raw frame payload. + * Available to subclasses (notably `SseStream::events()`) so they can + * construct typed envelopes without accessing the closure directly. + * + * @return T + */ + protected function deserialize(string $raw): mixed + { + return ($this->deserializer)($raw); + } + + /** + * @return Generator + */ + private function iterateSse(): Generator + { + foreach ($this->iterateRawSseEvents() as $raw) { + yield ($this->deserializer)($raw['data']); + } + } + + /** + * Iterates the SSE stream yielding raw envelopes with WHATWG metadata fields + * intact. Yields plain associative arrays — not `SseEvent` objects — so the + * data-only iteration path doesn't pay the allocation cost; `SseStream::events()` + * constructs the public `SseEvent` on top. + * + * Per WHATWG: the `id:` field persists across events within this iteration; + * the configured `terminator`, if present, ends iteration when matched against + * `data`. + * + * @internal + * @return Generator + */ + protected function iterateRawSseEvents(): Generator + { + $dataBuffer = ''; + $eventType = ''; + $lastEventId = ''; + $retry = null; + foreach ($this->readLines() as $line) { + if ($line === '') { + if ($dataBuffer === '') { + continue; + } + $payload = substr($dataBuffer, 0, -1); + $dataBuffer = ''; + if ($this->terminator !== null && $payload === $this->terminator) { + return; + } + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + // Per WHATWG: do NOT reset lastEventId between events. + $eventType = ''; + $retry = null; + continue; + } + if (str_starts_with($line, ':')) { + continue; + } + $colonPos = strpos($line, ':'); + if ($colonPos === false) { + if ($line === 'data') { + $dataBuffer = $this->appendWithinCap($dataBuffer, "\n"); + } + continue; + } + $field = substr($line, 0, $colonPos); + $value = substr($line, $colonPos + 1); + if (str_starts_with($value, ' ')) { + $value = substr($value, 1); + } + switch ($field) { + case 'data': + $dataBuffer = $this->appendWithinCap($dataBuffer, $value . "\n"); + break; + case 'event': + $eventType = $value; + break; + case 'id': + // WHATWG: ignore IDs that contain a NULL byte. + if (!str_contains($value, "\0")) { + $lastEventId = $value; + } + break; + case 'retry': + // WHATWG: ignore the value if it isn't a base-10 integer. + if ($value !== '' && ctype_digit($value)) { + $retry = (int) $value; + } + break; + } + } + // Flush a trailing event that lacked a closing blank line. + if ($dataBuffer !== '') { + $payload = substr($dataBuffer, 0, -1); + if ($this->terminator === null || $payload !== $this->terminator) { + yield ['data' => $payload, 'event' => $eventType, 'id' => $lastEventId, 'retry' => $retry]; + } + } + } + + /** + * @return Generator + */ + private function iterateDelimited(): Generator + { + foreach ($this->readLines() as $line) { + if ($line === '') { + continue; + } + if ($this->terminator !== null && $line === $this->terminator) { + return; + } + yield ($this->deserializer)($line); + } + } + + /** + * @return Generator + */ + private function iterateText(): Generator + { + foreach ($this->readLines() as $line) { + yield ($this->deserializer)($line); + } + } + + /** + * Reads the response body and yields complete lines, normalizing CRLF/CR + * to LF per the WHATWG SSE spec. Strips a single UTF-8 BOM if present at + * the start of the stream (WHATWG §9.2.4). Trailing partial content + * (without a terminating newline) is emitted as a final line. + * + * `$pendingCr` tracks whether the prior chunk's last byte was `\r`, so a + * `\r\n` sequence split across a read boundary collapses to one terminator + * instead of two. + * + * @return Generator + */ + private function readLines(): Generator + { + $buffer = ''; + $pendingCr = false; + $bomChecked = false; + while (!$this->body->eof()) { + $chunk = $this->body->read(self::READ_CHUNK_SIZE); + if ($chunk === '') { + continue; + } + if ($pendingCr && $chunk[0] === "\n") { + $chunk = substr($chunk, 1); + } + if (str_contains($chunk, "\r")) { + $pendingCr = str_ends_with($chunk, "\r"); + $chunk = str_replace(["\r\n", "\r"], "\n", $chunk); + } else { + $pendingCr = false; + } + $buffer = $this->appendWithinCap($buffer, $chunk); + if (!$bomChecked && strlen($buffer) >= 3) { + $bomChecked = true; + if (str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + } + while (($lfPos = strpos($buffer, "\n")) !== false) { + yield substr($buffer, 0, $lfPos); + $buffer = substr($buffer, $lfPos + 1); + } + } + // BOM may not have been checked yet if the entire body was < 3 bytes. + if (!$bomChecked && str_starts_with($buffer, self::UTF8_BOM)) { + $buffer = substr($buffer, 3); + } + if ($buffer !== '') { + yield $buffer; + } + } + + /** + * Appends $suffix to $buffer, throwing `RuntimeException` if the resulting + * size would exceed `maxBufferSize`. + */ + private function appendWithinCap(string $buffer, string $suffix): string + { + if (strlen($buffer) + strlen($suffix) > $this->maxBufferSize) { + throw new RuntimeException( + "Stream buffer would exceed maximum size of {$this->maxBufferSize} bytes", + ); + } + return $buffer . $suffix; + } + + public function __destruct() + { + try { + $this->body->close(); + } catch (\Throwable) { + // Best effort — the body may already be closed by the consumer. + } + } +} diff --git a/seed/php-sdk/streaming/src/Core/Client/StreamFormat.php b/seed/php-sdk/streaming/src/Core/Client/StreamFormat.php new file mode 100644 index 000000000000..2db924fefd94 --- /dev/null +++ b/seed/php-sdk/streaming/src/Core/Client/StreamFormat.php @@ -0,0 +1,13 @@ + + */ +class TextStream extends Stream +{ + /** + * @param ResponseInterface $response The HTTP response to stream from. + * @param int $maxBufferSize See `Stream::__construct`. Defaults to 1 MiB. + */ + public function __construct( + ResponseInterface $response, + int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE, + ) { + parent::__construct( + response: $response, + deserializer: fn (string $line): string => $line, + format: StreamFormat::Text, + terminator: null, + maxBufferSize: $maxBufferSize, + ); + } +} diff --git a/seed/php-sdk/streaming/src/Dummy/DummyClient.php b/seed/php-sdk/streaming/src/Dummy/DummyClient.php index 0367c79ef55f..e90a5433491a 100644 --- a/seed/php-sdk/streaming/src/Dummy/DummyClient.php +++ b/seed/php-sdk/streaming/src/Dummy/DummyClient.php @@ -5,13 +5,14 @@ use Psr\Http\Client\ClientInterface; use Seed\Core\Client\RawClient; use Seed\Dummy\Requests\GenerateStreamRequest; +use Seed\Core\Client\JsonStream; +use Seed\Dummy\Types\StreamResponse; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\Json\JsonApiRequest; use Seed\Core\Client\HttpMethod; use Psr\Http\Client\ClientExceptionInterface; use Seed\Dummy\Requests\Generateequest; -use Seed\Dummy\Types\StreamResponse; use JsonException; class DummyClient @@ -60,10 +61,11 @@ public function __construct( * queryParameters?: array, * bodyProperties?: array, * } $options + * @return JsonStream * @throws SeedException * @throws SeedApiException */ - public function generateStream(GenerateStreamRequest $request, ?array $options = null): void + public function generateStream(GenerateStreamRequest $request, ?array $options = null): JsonStream { $options = array_merge($this->options, $options ?? []); try { @@ -77,6 +79,9 @@ public function generateStream(GenerateStreamRequest $request, ?array $options = $options, ); $statusCode = $response->getStatusCode(); + if ($statusCode >= 200 && $statusCode < 400) { + return new JsonStream(response: $response, deserializer: fn (string $data) => StreamResponse::fromJson($data), terminator: null); + } } catch (ClientExceptionInterface $e) { throw new SeedException(message: $e->getMessage(), previous: $e); } diff --git a/seed/php-sdk/streaming/tests/Core/Client/StreamTest.php b/seed/php-sdk/streaming/tests/Core/Client/StreamTest.php new file mode 100644 index 000000000000..ed6ae35d9119 --- /dev/null +++ b/seed/php-sdk/streaming/tests/Core/Client/StreamTest.php @@ -0,0 +1,297 @@ + $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseConcatenatesMultilineDataWithNewlines(): void + { + $body = "data: line one\ndata: line two\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["line one\nline two"], iterator_to_array($stream, false)); + } + + public function testSseStripsLeadingSpaceFromFieldValues(): void + { + // SSE spec: a single leading space in the field value is stripped. + $body = "data: with-leading-space\ndata:no-leading-space\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(["with-leading-space\nno-leading-space"], iterator_to_array($stream, false)); + } + + public function testSseIgnoresCommentLines(): void + { + $body = ": this is a comment\ndata: payload\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['payload'], iterator_to_array($stream, false)); + } + + public function testSseTerminatorEndsIterationCleanly(): void + { + $body = "data: first\n\ndata: [DONE]\n\ndata: never-yielded\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, '[DONE]'); + + $this->assertSame(['first'], iterator_to_array($stream, false)); + } + + public function testSseNormalizesCrlfAndLoneCr(): void + { + $body = "data: a\r\n\r\ndata: b\rdata: c\r\r"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['a', "b\nc"], iterator_to_array($stream, false)); + } + + public function testSseDispatchesTrailingEventWithoutBlankLine(): void + { + $body = "data: incomplete"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['incomplete'], iterator_to_array($stream, false)); + } + + public function testSseAppliesDeserializerOncePerEvent(): void + { + $body = "data: {\"n\":1}\n\ndata: {\"n\":2}\n\n"; + $stream = new SseStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2]], iterator_to_array($stream, false)); + } + + public function testSseEventsExposesEventIdAndRetryMetadata(): void + { + $body = "event: chat\nid: msg-1\nretry: 5000\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertCount(1, $events); + $this->assertInstanceOf(SseEvent::class, $events[0]); + $this->assertSame('hi', $events[0]->data); + $this->assertSame('chat', $events[0]->event); + $this->assertSame('msg-1', $events[0]->id); + $this->assertSame(5000, $events[0]->retry); + } + + public function testSseEventsPersistsLastEventIdAcrossEventsPerSpec(): void + { + // Per WHATWG SSE: once an `id:` is set it persists across subsequent + // events until explicitly overridden. + $body = "id: persistent\ndata: a\n\ndata: b\n\nid: replaced\ndata: c\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['persistent', 'persistent', 'replaced'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresIdContainingNullByte(): void + { + $body = "id: ok\ndata: first\n\nid: bad\0id\ndata: second\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertSame(['ok', 'ok'], array_map(fn (SseEvent $e) => $e->id, $events)); + } + + public function testSseEventsIgnoresNonIntegerRetry(): void + { + $body = "retry: not-a-number\ndata: hi\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $events = iterator_to_array($stream->events(), false); + + $this->assertNull($events[0]->retry); + } + + public function testSseConstructorRejectsNonSseContentType(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/text\/event-stream/'); + + new SseStream( + self::response('data: x\n\n', contentType: 'application/json'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorRejectsNonUtf8Charset(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/charset/i'); + + new SseStream( + self::response('data: x\n\n', contentType: 'text/event-stream; charset=iso-8859-1'), + fn (string $d): string => $d, + null, + ); + } + + public function testSseConstructorAcceptsUtf8CharsetParameter(): void + { + $stream = new SseStream( + self::response("data: hi\n\n", contentType: 'text/event-stream; charset=UTF-8'), + fn (string $d): string => $d, + null, + ); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testSseConstructorToleratesMissingContentTypeHeader(): void + { + $response = \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withBody(\Http\Discovery\Psr17FactoryDiscovery::findStreamFactory()->createStream("data: hi\n\n")); + + $stream = new SseStream($response, fn (string $d): string => $d, null); + + $this->assertSame(['hi'], iterator_to_array($stream, false)); + } + + public function testStreamThrowsWhenLineBufferExceedsMaxSize(): void + { + $bigLine = str_repeat('A', 200) . "\n"; + $stream = new SseStream( + self::response("data: " . $bigLine . "\n"), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testStreamThrowsBeforeAccumulatingPastMaxBufferOnLongRunningSseEvent(): void + { + // Each line is well under the 64-byte cap; the cumulative `data:` + // append must trip the check before the buffer balloons. + $manyDataLines = ''; + for ($i = 0; $i < 50; $i++) { + $manyDataLines .= "data: chunk-$i\n"; + } + $stream = new SseStream( + self::response($manyDataLines), + fn (string $d): string => $d, + terminator: null, + maxBufferSize: 64, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/buffer/i'); + + iterator_to_array($stream, false); + } + + public function testSseStripsUtf8BomFromStartOfStream(): void + { + // WHATWG §9.2.4 requires a leading U+FEFF to be dropped. + $body = "\xEF\xBB\xBFdata: hello\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['hello'], iterator_to_array($stream, false)); + } + + public function testSseStripsBomOnlyAtVeryStart(): void + { + $body = "data: first\n\ndata: \xEF\xBB\xBFsecond\n\n"; + $stream = new SseStream(self::response($body), fn (string $d): string => $d, null); + + $this->assertSame(['first', "\xEF\xBB\xBFsecond"], iterator_to_array($stream, false)); + } + + public function testJsonStreamYieldsOnePerLine(): void + { + $body = "{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['n' => 1], ['n' => 2], ['n' => 3]], iterator_to_array($stream, false)); + } + + public function testJsonStreamSkipsEmptyLines(): void + { + $body = "{\"a\":1}\n\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), null); + + $this->assertSame([['a' => 1], ['a' => 2]], iterator_to_array($stream, false)); + } + + public function testJsonStreamTerminatorEndsIteration(): void + { + $body = "{\"a\":1}\n[DONE]\n{\"a\":2}\n"; + $stream = new JsonStream(self::response($body), self::jsonDecoder(), '[DONE]'); + + $this->assertSame([['a' => 1]], iterator_to_array($stream, false)); + } + + /** + * @return \Closure(string): array + */ + private static function jsonDecoder(): \Closure + { + return static function (string $raw): array { + /** @var array $decoded */ + $decoded = json_decode($raw, true); + return $decoded; + }; + } + + public function testTextStreamYieldsRawLines(): void + { + $body = "alpha\nbeta\ngamma\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', 'beta', 'gamma'], iterator_to_array($stream, false)); + } + + public function testTextStreamPreservesEmptyLines(): void + { + $body = "alpha\n\nbeta\n"; + $stream = new TextStream(self::response($body)); + + $this->assertSame(['alpha', '', 'beta'], iterator_to_array($stream, false)); + } + + /** + * Build a PSR-7 ResponseInterface with the given body. The Content-Type + * defaults to `text/event-stream` so SSE tests just work; pass an override + * for the validation-error cases. + */ + private static function response( + string $body, + string $contentType = 'text/event-stream', + ): ResponseInterface { + return \Http\Discovery\Psr17FactoryDiscovery::findResponseFactory() + ->createResponse(200) + ->withHeader('Content-Type', $contentType) + ->withBody( + \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory() + ->createStream($body), + ); + } +} diff --git a/seed/python-sdk/allof-inline/no-custom-config/poetry.lock b/seed/python-sdk/allof-inline/no-custom-config/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/allof-inline/no-custom-config/poetry.lock +++ b/seed/python-sdk/allof-inline/no-custom-config/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/allof/no-custom-config/poetry.lock b/seed/python-sdk/allof/no-custom-config/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/allof/no-custom-config/poetry.lock +++ b/seed/python-sdk/allof/no-custom-config/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/poetry.lock b/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/poetry.lock +++ b/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/exhaustive/deps_with_min_python_version/poetry.lock b/seed/python-sdk/exhaustive/deps_with_min_python_version/poetry.lock index 31d2a0dfcd72..120677f1ba91 100644 --- a/seed/python-sdk/exhaustive/deps_with_min_python_version/poetry.lock +++ b/seed/python-sdk/exhaustive/deps_with_min_python_version/poetry.lock @@ -2125,14 +2125,14 @@ files = [ [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/poetry.lock b/seed/python-sdk/exhaustive/extra_dev_dependencies/poetry.lock index f0c245b3a75d..bf864144824f 100644 --- a/seed/python-sdk/exhaustive/extra_dev_dependencies/poetry.lock +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/poetry.lock @@ -1378,14 +1378,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/exhaustive/no-custom-config/poetry.lock b/seed/python-sdk/exhaustive/no-custom-config/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/exhaustive/no-custom-config/poetry.lock +++ b/seed/python-sdk/exhaustive/no-custom-config/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/exhaustive/wire-tests-custom-client-name/poetry.lock b/seed/python-sdk/exhaustive/wire-tests-custom-client-name/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/exhaustive/wire-tests-custom-client-name/poetry.lock +++ b/seed/python-sdk/exhaustive/wire-tests-custom-client-name/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/imdb/reference.md b/seed/python-sdk/imdb/reference.md index 4be8cee400a7..77a07ff7459a 100644 --- a/seed/python-sdk/imdb/reference.md +++ b/seed/python-sdk/imdb/reference.md @@ -53,7 +53,15 @@ client.imdb.create_movie(
-**request:** `CreateMovieRequest` +**title:** `str` + +
+
+ +
+
+ +**rating:** `float`
diff --git a/seed/python-sdk/imdb/src/seed/__init__.py b/seed/python-sdk/imdb/src/seed/__init__.py index 65658493e08c..6e37906dda9d 100644 --- a/seed/python-sdk/imdb/src/seed/__init__.py +++ b/seed/python-sdk/imdb/src/seed/__init__.py @@ -6,19 +6,19 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .types import Movie, MovieId + from .errors import NotFoundError from . import imdb from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient from .client import AsyncSeedApi, SeedApi - from .imdb import CreateMovieRequest, Movie, MovieDoesNotExistError, MovieId from .version import __version__ _dynamic_imports: typing.Dict[str, str] = { "AsyncSeedApi": ".client", - "CreateMovieRequest": ".imdb", "DefaultAioHttpClient": "._default_clients", "DefaultAsyncHttpxClient": "._default_clients", - "Movie": ".imdb", - "MovieDoesNotExistError": ".imdb", - "MovieId": ".imdb", + "Movie": ".types", + "MovieId": ".types", + "NotFoundError": ".errors", "SeedApi": ".client", "__version__": ".version", "imdb": ".imdb", @@ -48,12 +48,11 @@ def __dir__(): __all__ = [ "AsyncSeedApi", - "CreateMovieRequest", "DefaultAioHttpClient", "DefaultAsyncHttpxClient", "Movie", - "MovieDoesNotExistError", "MovieId", + "NotFoundError", "SeedApi", "__version__", "imdb", diff --git a/seed/python-sdk/imdb/src/seed/imdb/errors/__init__.py b/seed/python-sdk/imdb/src/seed/errors/__init__.py similarity index 81% rename from seed/python-sdk/imdb/src/seed/imdb/errors/__init__.py rename to seed/python-sdk/imdb/src/seed/errors/__init__.py index 109d62c275d2..7d82d5b69ba8 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/errors/__init__.py +++ b/seed/python-sdk/imdb/src/seed/errors/__init__.py @@ -6,8 +6,8 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .movie_does_not_exist_error import MovieDoesNotExistError -_dynamic_imports: typing.Dict[str, str] = {"MovieDoesNotExistError": ".movie_does_not_exist_error"} + from .not_found_error import NotFoundError +_dynamic_imports: typing.Dict[str, str] = {"NotFoundError": ".not_found_error"} def __getattr__(attr_name: str) -> typing.Any: @@ -31,4 +31,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["MovieDoesNotExistError"] +__all__ = ["NotFoundError"] diff --git a/seed/python-sdk/imdb/src/seed/imdb/errors/movie_does_not_exist_error.py b/seed/python-sdk/imdb/src/seed/errors/not_found_error.py similarity index 78% rename from seed/python-sdk/imdb/src/seed/imdb/errors/movie_does_not_exist_error.py rename to seed/python-sdk/imdb/src/seed/errors/not_found_error.py index e235e61be6cb..7951178da2f0 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/errors/movie_does_not_exist_error.py +++ b/seed/python-sdk/imdb/src/seed/errors/not_found_error.py @@ -2,10 +2,10 @@ import typing -from ...core.api_error import ApiError +from ..core.api_error import ApiError from ..types.movie_id import MovieId -class MovieDoesNotExistError(ApiError): +class NotFoundError(ApiError): def __init__(self, body: MovieId, headers: typing.Optional[typing.Dict[str, str]] = None): super().__init__(status_code=404, headers=headers, body=body) diff --git a/seed/python-sdk/imdb/src/seed/imdb/__init__.py b/seed/python-sdk/imdb/src/seed/imdb/__init__.py index 6c3b4cf787b9..5cde0202dcf3 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/__init__.py +++ b/seed/python-sdk/imdb/src/seed/imdb/__init__.py @@ -2,39 +2,3 @@ # isort: skip_file -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import CreateMovieRequest, Movie, MovieId - from .errors import MovieDoesNotExistError -_dynamic_imports: typing.Dict[str, str] = { - "CreateMovieRequest": ".types", - "Movie": ".types", - "MovieDoesNotExistError": ".errors", - "MovieId": ".types", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["CreateMovieRequest", "Movie", "MovieDoesNotExistError", "MovieId"] diff --git a/seed/python-sdk/imdb/src/seed/imdb/client.py b/seed/python-sdk/imdb/src/seed/imdb/client.py index 35206c5a432c..1a176e1692b1 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/client.py +++ b/seed/python-sdk/imdb/src/seed/imdb/client.py @@ -4,9 +4,9 @@ from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions +from ..types.movie import Movie +from ..types.movie_id import MovieId from .raw_client import AsyncRawImdbClient, RawImdbClient -from .types.movie import Movie -from .types.movie_id import MovieId # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -45,6 +45,7 @@ def create_movie( Returns ------- MovieId + Success Examples -------- @@ -74,6 +75,7 @@ def get_movie(self, movie_id: MovieId, *, request_options: typing.Optional[Reque Returns ------- Movie + Success Examples -------- @@ -124,6 +126,7 @@ async def create_movie( Returns ------- MovieId + Success Examples -------- @@ -161,6 +164,7 @@ async def get_movie(self, movie_id: MovieId, *, request_options: typing.Optional Returns ------- Movie + Success Examples -------- diff --git a/seed/python-sdk/imdb/src/seed/imdb/raw_client.py b/seed/python-sdk/imdb/src/seed/imdb/raw_client.py index d08a6c806a66..a5dc39c7657d 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/raw_client.py +++ b/seed/python-sdk/imdb/src/seed/imdb/raw_client.py @@ -10,9 +10,9 @@ from ..core.parse_error import ParsingError from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions -from .errors.movie_does_not_exist_error import MovieDoesNotExistError -from .types.movie import Movie -from .types.movie_id import MovieId +from ..errors.not_found_error import NotFoundError +from ..types.movie import Movie +from ..types.movie_id import MovieId from pydantic import ValidationError # this is used as the default value for optional parameters @@ -41,6 +41,7 @@ def create_movie( Returns ------- HttpResponse[MovieId] + Success """ _response = self._client_wrapper.httpx_client.request( "movies/create-movie", @@ -49,6 +50,9 @@ def create_movie( "title": title, "rating": rating, }, + headers={ + "content-type": "application/json", + }, request_options=request_options, omit=OMIT, ) @@ -85,6 +89,7 @@ def get_movie( Returns ------- HttpResponse[Movie] + Success """ _response = self._client_wrapper.httpx_client.request( f"movies/{encode_path_param(movie_id)}", @@ -102,7 +107,7 @@ def get_movie( ) return HttpResponse(response=_response, data=_data) if _response.status_code == 404: - raise MovieDoesNotExistError( + raise NotFoundError( headers=dict(_response.headers), body=typing.cast( MovieId, @@ -144,6 +149,7 @@ async def create_movie( Returns ------- AsyncHttpResponse[MovieId] + Success """ _response = await self._client_wrapper.httpx_client.request( "movies/create-movie", @@ -152,6 +158,9 @@ async def create_movie( "title": title, "rating": rating, }, + headers={ + "content-type": "application/json", + }, request_options=request_options, omit=OMIT, ) @@ -188,6 +197,7 @@ async def get_movie( Returns ------- AsyncHttpResponse[Movie] + Success """ _response = await self._client_wrapper.httpx_client.request( f"movies/{encode_path_param(movie_id)}", @@ -205,7 +215,7 @@ async def get_movie( ) return AsyncHttpResponse(response=_response, data=_data) if _response.status_code == 404: - raise MovieDoesNotExistError( + raise NotFoundError( headers=dict(_response.headers), body=typing.cast( MovieId, diff --git a/seed/python-sdk/imdb/src/seed/imdb/types/create_movie_request.py b/seed/python-sdk/imdb/src/seed/imdb/types/create_movie_request.py deleted file mode 100644 index 8d4657a0d69e..000000000000 --- a/seed/python-sdk/imdb/src/seed/imdb/types/create_movie_request.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class CreateMovieRequest(UniversalBaseModel): - title: str - rating: float - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/seed/python-sdk/imdb/src/seed/imdb/types/__init__.py b/seed/python-sdk/imdb/src/seed/types/__init__.py similarity index 79% rename from seed/python-sdk/imdb/src/seed/imdb/types/__init__.py rename to seed/python-sdk/imdb/src/seed/types/__init__.py index a10206054754..c48d2c41131b 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/types/__init__.py +++ b/seed/python-sdk/imdb/src/seed/types/__init__.py @@ -6,14 +6,9 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .create_movie_request import CreateMovieRequest from .movie import Movie from .movie_id import MovieId -_dynamic_imports: typing.Dict[str, str] = { - "CreateMovieRequest": ".create_movie_request", - "Movie": ".movie", - "MovieId": ".movie_id", -} +_dynamic_imports: typing.Dict[str, str] = {"Movie": ".movie", "MovieId": ".movie_id"} def __getattr__(attr_name: str) -> typing.Any: @@ -37,4 +32,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["CreateMovieRequest", "Movie", "MovieId"] +__all__ = ["Movie", "MovieId"] diff --git a/seed/python-sdk/imdb/src/seed/imdb/types/movie.py b/seed/python-sdk/imdb/src/seed/types/movie.py similarity index 88% rename from seed/python-sdk/imdb/src/seed/imdb/types/movie.py rename to seed/python-sdk/imdb/src/seed/types/movie.py index c7b15656c7e6..ec1699ff7a80 100644 --- a/seed/python-sdk/imdb/src/seed/imdb/types/movie.py +++ b/seed/python-sdk/imdb/src/seed/types/movie.py @@ -3,7 +3,7 @@ import typing import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .movie_id import MovieId diff --git a/seed/python-sdk/imdb/src/seed/imdb/types/movie_id.py b/seed/python-sdk/imdb/src/seed/types/movie_id.py similarity index 100% rename from seed/python-sdk/imdb/src/seed/imdb/types/movie_id.py rename to seed/python-sdk/imdb/src/seed/types/movie_id.py diff --git a/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/poetry.lock b/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/poetry.lock +++ b/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/server-sent-events-openapi/with-wire-tests/poetry.lock b/seed/python-sdk/server-sent-events-openapi/with-wire-tests/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/server-sent-events-openapi/with-wire-tests/poetry.lock +++ b/seed/python-sdk/server-sent-events-openapi/with-wire-tests/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/server-sent-events/with-wire-tests/poetry.lock b/seed/python-sdk/server-sent-events/with-wire-tests/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/server-sent-events/with-wire-tests/poetry.lock +++ b/seed/python-sdk/server-sent-events/with-wire-tests/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/python-sdk/unions/union-naming-v1-wire-tests/poetry.lock b/seed/python-sdk/unions/union-naming-v1-wire-tests/poetry.lock index 4d088635c3c7..6203cfe20327 100644 --- a/seed/python-sdk/unions/union-naming-v1-wire-tests/poetry.lock +++ b/seed/python-sdk/unions/union-naming-v1-wire-tests/poetry.lock @@ -1326,14 +1326,14 @@ six = ">=1.5" [[package]] name = "requests" -version = "2.34.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, - {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] diff --git a/seed/ruby-sdk-v2/imdb/dynamic-snippets/example1/snippet.rb b/seed/ruby-sdk-v2/imdb/dynamic-snippets/example1/snippet.rb index 38291880b8cd..7cc7a67f455f 100644 --- a/seed/ruby-sdk-v2/imdb/dynamic-snippets/example1/snippet.rb +++ b/seed/ruby-sdk-v2/imdb/dynamic-snippets/example1/snippet.rb @@ -5,4 +5,7 @@ base_url: "https://api.fern.com" ) -client.imdb.get_movie(movie_id: "movieId") +client.imdb.create_movie( + title: "title", + rating: 1.1 +) diff --git a/seed/ruby-sdk-v2/imdb/dynamic-snippets/example3/snippet.rb b/seed/ruby-sdk-v2/imdb/dynamic-snippets/example3/snippet.rb new file mode 100644 index 000000000000..38291880b8cd --- /dev/null +++ b/seed/ruby-sdk-v2/imdb/dynamic-snippets/example3/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + token: "", + base_url: "https://api.fern.com" +) + +client.imdb.get_movie(movie_id: "movieId") diff --git a/seed/ruby-sdk-v2/imdb/dynamic-snippets/example4/snippet.rb b/seed/ruby-sdk-v2/imdb/dynamic-snippets/example4/snippet.rb new file mode 100644 index 000000000000..38291880b8cd --- /dev/null +++ b/seed/ruby-sdk-v2/imdb/dynamic-snippets/example4/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + token: "", + base_url: "https://api.fern.com" +) + +client.imdb.get_movie(movie_id: "movieId") diff --git a/seed/ruby-sdk-v2/imdb/lib/seed.rb b/seed/ruby-sdk-v2/imdb/lib/seed.rb index b4cababd6e81..b6b9b661b3e0 100644 --- a/seed/ruby-sdk-v2/imdb/lib/seed.rb +++ b/seed/ruby-sdk-v2/imdb/lib/seed.rb @@ -35,8 +35,9 @@ require_relative "seed/internal/iterators/offset_item_iterator" require_relative "seed/internal/iterators/cursor_page_iterator" require_relative "seed/internal/iterators/offset_page_iterator" -require_relative "seed/imdb/types/movie_id" -require_relative "seed/imdb/types/movie" -require_relative "seed/imdb/types/create_movie_request" +require_relative "seed/types/movie_id" +require_relative "seed/types/movie" require_relative "seed/client" require_relative "seed/imdb/client" +require_relative "seed/imdb/types/create_movie_request" +require_relative "seed/imdb/types/get_movie_imdb_request" diff --git a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/client.rb b/seed/ruby-sdk-v2/imdb/lib/seed/imdb/client.rb index 7ff7b967e532..d1ebd829fa0a 100644 --- a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/client.rb +++ b/seed/ruby-sdk-v2/imdb/lib/seed/imdb/client.rb @@ -26,7 +26,7 @@ def create_movie(request_options: {}, **params) request = Seed::Internal::JSON::Request.new( base_url: request_options[:base_url], method: "POST", - path: "/movies/create-movie", + path: "movies/create-movie", body: Seed::Imdb::Types::CreateMovieRequest.new(params).to_h, request_options: request_options ) @@ -37,7 +37,7 @@ def create_movie(request_options: {}, **params) end code = response.code.to_i if code.between?(200, 299) - Seed::Imdb::Types::MovieID.load(response.body) + Seed::Types::MovieID.load(response.body) else error_class = Seed::Errors::ResponseError.subclass_for_code(code) raise error_class.new(response.body, code: code) @@ -51,15 +51,15 @@ def create_movie(request_options: {}, **params) # @option request_options [Hash{String => Object}] :additional_query_parameters # @option request_options [Hash{String => Object}] :additional_body_parameters # @option request_options [Integer] :timeout_in_seconds - # @option params [Seed::Imdb::Types::MovieID] :movie_id + # @option params [Seed::Types::MovieID] :movie_id # - # @return [Seed::Imdb::Types::Movie] + # @return [Seed::Types::Movie] def get_movie(request_options: {}, **params) params = Seed::Internal::Types::Utils.normalize_keys(params) request = Seed::Internal::JSON::Request.new( base_url: request_options[:base_url], method: "GET", - path: "/movies/#{URI.encode_uri_component(params[:movie_id].to_s)}", + path: "movies/#{URI.encode_uri_component(params[:movie_id].to_s)}", request_options: request_options ) begin @@ -69,7 +69,7 @@ def get_movie(request_options: {}, **params) end code = response.code.to_i if code.between?(200, 299) - Seed::Imdb::Types::Movie.load(response.body) + Seed::Types::Movie.load(response.body) else error_class = Seed::Errors::ResponseError.subclass_for_code(code) raise error_class.new(response.body, code: code) diff --git a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/get_movie_imdb_request.rb b/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/get_movie_imdb_request.rb new file mode 100644 index 000000000000..21163c0ba1de --- /dev/null +++ b/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/get_movie_imdb_request.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Seed + module Imdb + module Types + class GetMovieImdbRequest < Internal::Types::Model + field :movie_id, -> { String }, optional: false, nullable: false, api_name: "movieId" + end + end + end +end diff --git a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/movie.rb b/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/movie.rb deleted file mode 100644 index 6627480b4a64..000000000000 --- a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/movie.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Seed - module Imdb - module Types - class Movie < Internal::Types::Model - field :id, -> { String }, optional: false, nullable: false - - field :title, -> { String }, optional: false, nullable: false - - field :rating, -> { Integer }, optional: false, nullable: false - end - end - end -end diff --git a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/movie_id.rb b/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/movie_id.rb deleted file mode 100644 index 8ec840aa0c66..000000000000 --- a/seed/ruby-sdk-v2/imdb/lib/seed/imdb/types/movie_id.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Seed - module Imdb - module Types - module MovieID - # MovieID is an alias for String - - # @option str [String] - # - # @return [untyped] - def self.load(str) - ::JSON.parse(str) - end - - # @option value [untyped] - # - # @return [String] - def self.dump(value) - ::JSON.generate(value) - end - end - end - end -end diff --git a/seed/ruby-sdk-v2/imdb/lib/seed/types/movie.rb b/seed/ruby-sdk-v2/imdb/lib/seed/types/movie.rb new file mode 100644 index 000000000000..b8bd9fd88133 --- /dev/null +++ b/seed/ruby-sdk-v2/imdb/lib/seed/types/movie.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Seed + module Types + class Movie < Internal::Types::Model + field :id, -> { String }, optional: false, nullable: false + + field :title, -> { String }, optional: false, nullable: false + + field :rating, -> { Integer }, optional: false, nullable: false + end + end +end diff --git a/seed/ruby-sdk-v2/imdb/lib/seed/types/movie_id.rb b/seed/ruby-sdk-v2/imdb/lib/seed/types/movie_id.rb new file mode 100644 index 000000000000..0127ced3a065 --- /dev/null +++ b/seed/ruby-sdk-v2/imdb/lib/seed/types/movie_id.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Seed + module Types + module MovieID + # MovieID is an alias for String + + # @option str [String] + # + # @return [untyped] + def self.load(str) + ::JSON.parse(str) + end + + # @option value [untyped] + # + # @return [String] + def self.dump(value) + ::JSON.generate(value) + end + end + end +end diff --git a/seed/ruby-sdk-v2/imdb/reference.md b/seed/ruby-sdk-v2/imdb/reference.md index ada4c3d9c1b6..6d003f44838b 100644 --- a/seed/ruby-sdk-v2/imdb/reference.md +++ b/seed/ruby-sdk-v2/imdb/reference.md @@ -45,7 +45,15 @@ client.imdb.create_movie(
-**request:** `Seed::Imdb::Types::CreateMovieRequest` +**title:** `String` + +
+
+ +
+
+ +**rating:** `Integer`
@@ -65,7 +73,7 @@ client.imdb.create_movie(
-
client.imdb.get_movie(movie_id) -> Seed::Imdb::Types::Movie +
client.imdb.get_movie(movie_id) -> Seed::Types::Movie
diff --git a/seed/rust-sdk/imdb/imdb-custom-config/README.md b/seed/rust-sdk/imdb/imdb-custom-config/README.md index ac5694ac347d..cdcbcc4b999d 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/README.md +++ b/seed/rust-sdk/imdb/imdb-custom-config/README.md @@ -11,6 +11,7 @@ The Seed Rust library provides convenient access to the Seed APIs from Rust. - [Reference](#reference) - [Usage](#usage) - [Errors](#errors) +- [Request Types](#request-types) - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) @@ -57,7 +58,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) @@ -83,6 +83,18 @@ match client.imdb.create_movie(None)?.await { } ``` +## Request Types + +The SDK exports all request types as Rust structs. Simply import them from the crate to access them: + +```rust +use custom_imdb_sdk::prelude::{*}; + +let request = CreateMovieRequest { + ... +}; +``` + ## Advanced ### Retries diff --git a/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example0.rs b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example0.rs index 55066f02c6df..17135bf1242c 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example0.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example0.rs @@ -14,7 +14,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) diff --git a/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example1.rs b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example1.rs index c8035a2a5176..17135bf1242c 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example1.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example1.rs @@ -10,6 +10,12 @@ async fn main() { let client = CustomImdbClient::new(config).expect("Failed to build client"); client .imdb - .get_movie(&MovieId("movieId".to_string()), None) + .create_movie( + &CreateMovieRequest { + title: "title".to_string(), + rating: 1.1, + }, + None, + ) .await; } diff --git a/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example3.rs b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example3.rs new file mode 100644 index 000000000000..c8035a2a5176 --- /dev/null +++ b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example3.rs @@ -0,0 +1,15 @@ +use custom_imdb_sdk::prelude::*; + +#[tokio::main] +async fn main() { + let config = ClientConfig { + base_url: "https://api.fern.com".to_string(), + token: Some("".to_string()), + ..Default::default() + }; + let client = CustomImdbClient::new(config).expect("Failed to build client"); + client + .imdb + .get_movie(&MovieId("movieId".to_string()), None) + .await; +} diff --git a/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example4.rs b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example4.rs new file mode 100644 index 000000000000..c8035a2a5176 --- /dev/null +++ b/seed/rust-sdk/imdb/imdb-custom-config/dynamic-snippets/example4.rs @@ -0,0 +1,15 @@ +use custom_imdb_sdk::prelude::*; + +#[tokio::main] +async fn main() { + let config = ClientConfig { + base_url: "https://api.fern.com".to_string(), + token: Some("".to_string()), + ..Default::default() + }; + let client = CustomImdbClient::new(config).expect("Failed to build client"); + client + .imdb + .get_movie(&MovieId("movieId".to_string()), None) + .await; +} diff --git a/seed/rust-sdk/imdb/imdb-custom-config/reference.md b/seed/rust-sdk/imdb/imdb-custom-config/reference.md index 25a3c0d5756d..e07056487241 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/reference.md +++ b/seed/rust-sdk/imdb/imdb-custom-config/reference.md @@ -42,7 +42,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) @@ -54,6 +53,29 @@ async fn main() {
+#### ⚙️ Parameters + +
+
+ +
+
+ +**title:** `String` + +
+
+ +
+
+ +**rating:** `f64` + +
+
+
+
+
diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/api/mod.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/api/mod.rs index 3a6d88e2753f..f98f5e7d9a11 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/src/api/mod.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/src/api/mod.rs @@ -1,4 +1,4 @@ -//! API client and types for the Api +//! API client and types for the api //! //! This module contains all the API definitions including request/response types //! and client implementations for interacting with the API. diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/api/resources/imdb/imdb.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/api/resources/imdb/imdb.rs index 30d111e14f60..6439dd3910ca 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/src/api/resources/imdb/imdb.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/src/api/resources/imdb/imdb.rs @@ -30,7 +30,7 @@ impl ImdbClient { self.http_client .execute_request( Method::POST, - "/movies/create-movie", + "movies/create-movie", Some(serde_json::to_value(request).map_err(ApiError::Serialization)?), None, options, @@ -46,7 +46,7 @@ impl ImdbClient { self.http_client .execute_request( Method::GET, - &format!("/movies/{}", movie_id.0), + &format!("movies/{}", movie_id.0), None, None, options, diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/imdb_create_movie_request.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/create_movie_request.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb-custom-config/src/api/types/imdb_create_movie_request.rs rename to seed/rust-sdk/imdb/imdb-custom-config/src/api/types/create_movie_request.rs diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/mod.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/mod.rs index f78038c8deaa..e13ee0fdcdac 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/mod.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/mod.rs @@ -1,7 +1,7 @@ -pub mod imdb_create_movie_request; -pub mod imdb_movie; -pub mod imdb_movie_id; +pub mod create_movie_request; +pub mod movie; +pub mod movie_id; -pub use imdb_create_movie_request::CreateMovieRequest; -pub use imdb_movie::Movie; -pub use imdb_movie_id::MovieId; +pub use create_movie_request::CreateMovieRequest; +pub use movie::Movie; +pub use movie_id::MovieId; diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/imdb_movie.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/movie.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb-custom-config/src/api/types/imdb_movie.rs rename to seed/rust-sdk/imdb/imdb-custom-config/src/api/types/movie.rs diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/imdb_movie_id.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/api/types/movie_id.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb-custom-config/src/api/types/imdb_movie_id.rs rename to seed/rust-sdk/imdb/imdb-custom-config/src/api/types/movie_id.rs diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/error.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/error.rs index d953af0246f6..621cc2bcd786 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/src/error.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/src/error.rs @@ -2,8 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { - #[error("MovieDoesNotExistError: Resource not found - {{message}}")] - MovieDoesNotExistError { + #[error("NotFoundError: Resource not found - {{message}}")] + NotFoundError { message: String, resource_id: Option, resource_type: Option, @@ -32,10 +32,10 @@ impl ApiError { pub fn from_response(status_code: u16, body: Option<&str>) -> Self { match status_code { 404 => { - // Parse error body for MovieDoesNotExistError; + // Parse error body for NotFoundError; if let Some(body_str) = body { if let Ok(parsed) = serde_json::from_str::(body_str) { - return Self::MovieDoesNotExistError { + return Self::NotFoundError { message: parsed .get("message") .and_then(|v| v.as_str()) @@ -50,7 +50,7 @@ impl ApiError { }; } } - return Self::MovieDoesNotExistError { + return Self::NotFoundError { message: body.unwrap_or("Unknown error").to_string(), resource_id: None, resource_type: None, diff --git a/seed/rust-sdk/imdb/imdb-custom-config/src/lib.rs b/seed/rust-sdk/imdb/imdb-custom-config/src/lib.rs index 1f4ebfbd7ebb..de5e5e510098 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/src/lib.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/src/lib.rs @@ -1,6 +1,6 @@ -//! # Api SDK +//! # api SDK //! -//! The official Rust SDK for the Api. +//! The official Rust SDK for the api. //! //! ## Getting Started //! @@ -20,7 +20,6 @@ //! &CreateMovieRequest { //! title: "title".to_string(), //! rating: 1.1, -//! ..Default::default() //! }, //! None, //! ) diff --git a/seed/rust-sdk/imdb/imdb-custom-config/tests/imdb_test.rs b/seed/rust-sdk/imdb/imdb-custom-config/tests/imdb_test.rs index 3e0c2950d825..cce7bc2d313f 100644 --- a/seed/rust-sdk/imdb/imdb-custom-config/tests/imdb_test.rs +++ b/seed/rust-sdk/imdb/imdb-custom-config/tests/imdb_test.rs @@ -21,7 +21,6 @@ async fn test_imdb_create_movie_with_wiremock() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) diff --git a/seed/rust-sdk/imdb/imdb-custom-features/README.md b/seed/rust-sdk/imdb/imdb-custom-features/README.md index a071339f5881..60f0d1ff2e89 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/README.md +++ b/seed/rust-sdk/imdb/imdb-custom-features/README.md @@ -11,6 +11,7 @@ The Seed Rust library provides convenient access to the Seed APIs from Rust. - [Reference](#reference) - [Usage](#usage) - [Errors](#errors) +- [Request Types](#request-types) - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) @@ -57,7 +58,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) @@ -83,6 +83,18 @@ match client.imdb.create_movie(None)?.await { } ``` +## Request Types + +The SDK exports all request types as Rust structs. Simply import them from the crate to access them: + +```rust +use seed_api::prelude::{*}; + +let request = CreateMovieRequest { + ... +}; +``` + ## Advanced ### Retries diff --git a/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example0.rs b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example0.rs index 3f85eda45163..3aa2fa52c695 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example0.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example0.rs @@ -14,7 +14,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) diff --git a/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example1.rs b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example1.rs index 3d7fd924405f..3aa2fa52c695 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example1.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example1.rs @@ -10,6 +10,12 @@ async fn main() { let client = ApiClient::new(config).expect("Failed to build client"); client .imdb - .get_movie(&MovieId("movieId".to_string()), None) + .create_movie( + &CreateMovieRequest { + title: "title".to_string(), + rating: 1.1, + }, + None, + ) .await; } diff --git a/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example3.rs b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example3.rs new file mode 100644 index 000000000000..3d7fd924405f --- /dev/null +++ b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example3.rs @@ -0,0 +1,15 @@ +use seed_api::prelude::*; + +#[tokio::main] +async fn main() { + let config = ClientConfig { + base_url: "https://api.fern.com".to_string(), + token: Some("".to_string()), + ..Default::default() + }; + let client = ApiClient::new(config).expect("Failed to build client"); + client + .imdb + .get_movie(&MovieId("movieId".to_string()), None) + .await; +} diff --git a/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example4.rs b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example4.rs new file mode 100644 index 000000000000..3d7fd924405f --- /dev/null +++ b/seed/rust-sdk/imdb/imdb-custom-features/dynamic-snippets/example4.rs @@ -0,0 +1,15 @@ +use seed_api::prelude::*; + +#[tokio::main] +async fn main() { + let config = ClientConfig { + base_url: "https://api.fern.com".to_string(), + token: Some("".to_string()), + ..Default::default() + }; + let client = ApiClient::new(config).expect("Failed to build client"); + client + .imdb + .get_movie(&MovieId("movieId".to_string()), None) + .await; +} diff --git a/seed/rust-sdk/imdb/imdb-custom-features/reference.md b/seed/rust-sdk/imdb/imdb-custom-features/reference.md index 1e9daddbd322..a1be5a666904 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/reference.md +++ b/seed/rust-sdk/imdb/imdb-custom-features/reference.md @@ -42,7 +42,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) @@ -54,6 +53,29 @@ async fn main() {
+#### ⚙️ Parameters + +
+
+ +
+
+ +**title:** `String` + +
+
+ +
+
+ +**rating:** `f64` + +
+
+
+
+
diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/api/mod.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/api/mod.rs index c1870d0f50e7..9999104a464b 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/src/api/mod.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/src/api/mod.rs @@ -1,4 +1,4 @@ -//! API client and types for the Api +//! API client and types for the api //! //! This module contains all the API definitions including request/response types //! and client implementations for interacting with the API. diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/api/resources/imdb/imdb.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/api/resources/imdb/imdb.rs index 30d111e14f60..6439dd3910ca 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/src/api/resources/imdb/imdb.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/src/api/resources/imdb/imdb.rs @@ -30,7 +30,7 @@ impl ImdbClient { self.http_client .execute_request( Method::POST, - "/movies/create-movie", + "movies/create-movie", Some(serde_json::to_value(request).map_err(ApiError::Serialization)?), None, options, @@ -46,7 +46,7 @@ impl ImdbClient { self.http_client .execute_request( Method::GET, - &format!("/movies/{}", movie_id.0), + &format!("movies/{}", movie_id.0), None, None, options, diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/imdb_create_movie_request.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/create_movie_request.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb-custom-features/src/api/types/imdb_create_movie_request.rs rename to seed/rust-sdk/imdb/imdb-custom-features/src/api/types/create_movie_request.rs diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/mod.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/mod.rs index f78038c8deaa..e13ee0fdcdac 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/mod.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/mod.rs @@ -1,7 +1,7 @@ -pub mod imdb_create_movie_request; -pub mod imdb_movie; -pub mod imdb_movie_id; +pub mod create_movie_request; +pub mod movie; +pub mod movie_id; -pub use imdb_create_movie_request::CreateMovieRequest; -pub use imdb_movie::Movie; -pub use imdb_movie_id::MovieId; +pub use create_movie_request::CreateMovieRequest; +pub use movie::Movie; +pub use movie_id::MovieId; diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/imdb_movie.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/movie.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb-custom-features/src/api/types/imdb_movie.rs rename to seed/rust-sdk/imdb/imdb-custom-features/src/api/types/movie.rs diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/imdb_movie_id.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/api/types/movie_id.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb-custom-features/src/api/types/imdb_movie_id.rs rename to seed/rust-sdk/imdb/imdb-custom-features/src/api/types/movie_id.rs diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/error.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/error.rs index d953af0246f6..621cc2bcd786 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/src/error.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/src/error.rs @@ -2,8 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { - #[error("MovieDoesNotExistError: Resource not found - {{message}}")] - MovieDoesNotExistError { + #[error("NotFoundError: Resource not found - {{message}}")] + NotFoundError { message: String, resource_id: Option, resource_type: Option, @@ -32,10 +32,10 @@ impl ApiError { pub fn from_response(status_code: u16, body: Option<&str>) -> Self { match status_code { 404 => { - // Parse error body for MovieDoesNotExistError; + // Parse error body for NotFoundError; if let Some(body_str) = body { if let Ok(parsed) = serde_json::from_str::(body_str) { - return Self::MovieDoesNotExistError { + return Self::NotFoundError { message: parsed .get("message") .and_then(|v| v.as_str()) @@ -50,7 +50,7 @@ impl ApiError { }; } } - return Self::MovieDoesNotExistError { + return Self::NotFoundError { message: body.unwrap_or("Unknown error").to_string(), resource_id: None, resource_type: None, diff --git a/seed/rust-sdk/imdb/imdb-custom-features/src/lib.rs b/seed/rust-sdk/imdb/imdb-custom-features/src/lib.rs index b55485610f13..72d1f9c1851b 100644 --- a/seed/rust-sdk/imdb/imdb-custom-features/src/lib.rs +++ b/seed/rust-sdk/imdb/imdb-custom-features/src/lib.rs @@ -1,6 +1,6 @@ -//! # Api SDK +//! # api SDK //! -//! The official Rust SDK for the Api. +//! The official Rust SDK for the api. //! //! ## Getting Started //! @@ -20,7 +20,6 @@ //! &CreateMovieRequest { //! title: "title".to_string(), //! rating: 1.1, -//! ..Default::default() //! }, //! None, //! ) diff --git a/seed/rust-sdk/imdb/imdb/README.md b/seed/rust-sdk/imdb/imdb/README.md index a071339f5881..60f0d1ff2e89 100644 --- a/seed/rust-sdk/imdb/imdb/README.md +++ b/seed/rust-sdk/imdb/imdb/README.md @@ -11,6 +11,7 @@ The Seed Rust library provides convenient access to the Seed APIs from Rust. - [Reference](#reference) - [Usage](#usage) - [Errors](#errors) +- [Request Types](#request-types) - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) @@ -57,7 +58,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) @@ -83,6 +83,18 @@ match client.imdb.create_movie(None)?.await { } ``` +## Request Types + +The SDK exports all request types as Rust structs. Simply import them from the crate to access them: + +```rust +use seed_api::prelude::{*}; + +let request = CreateMovieRequest { + ... +}; +``` + ## Advanced ### Retries diff --git a/seed/rust-sdk/imdb/imdb/dynamic-snippets/example0.rs b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example0.rs index 3f85eda45163..3aa2fa52c695 100644 --- a/seed/rust-sdk/imdb/imdb/dynamic-snippets/example0.rs +++ b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example0.rs @@ -14,7 +14,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) diff --git a/seed/rust-sdk/imdb/imdb/dynamic-snippets/example1.rs b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example1.rs index 3d7fd924405f..3aa2fa52c695 100644 --- a/seed/rust-sdk/imdb/imdb/dynamic-snippets/example1.rs +++ b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example1.rs @@ -10,6 +10,12 @@ async fn main() { let client = ApiClient::new(config).expect("Failed to build client"); client .imdb - .get_movie(&MovieId("movieId".to_string()), None) + .create_movie( + &CreateMovieRequest { + title: "title".to_string(), + rating: 1.1, + }, + None, + ) .await; } diff --git a/seed/rust-sdk/imdb/imdb/dynamic-snippets/example3.rs b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example3.rs new file mode 100644 index 000000000000..3d7fd924405f --- /dev/null +++ b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example3.rs @@ -0,0 +1,15 @@ +use seed_api::prelude::*; + +#[tokio::main] +async fn main() { + let config = ClientConfig { + base_url: "https://api.fern.com".to_string(), + token: Some("".to_string()), + ..Default::default() + }; + let client = ApiClient::new(config).expect("Failed to build client"); + client + .imdb + .get_movie(&MovieId("movieId".to_string()), None) + .await; +} diff --git a/seed/rust-sdk/imdb/imdb/dynamic-snippets/example4.rs b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example4.rs new file mode 100644 index 000000000000..3d7fd924405f --- /dev/null +++ b/seed/rust-sdk/imdb/imdb/dynamic-snippets/example4.rs @@ -0,0 +1,15 @@ +use seed_api::prelude::*; + +#[tokio::main] +async fn main() { + let config = ClientConfig { + base_url: "https://api.fern.com".to_string(), + token: Some("".to_string()), + ..Default::default() + }; + let client = ApiClient::new(config).expect("Failed to build client"); + client + .imdb + .get_movie(&MovieId("movieId".to_string()), None) + .await; +} diff --git a/seed/rust-sdk/imdb/imdb/reference.md b/seed/rust-sdk/imdb/imdb/reference.md index 1e9daddbd322..a1be5a666904 100644 --- a/seed/rust-sdk/imdb/imdb/reference.md +++ b/seed/rust-sdk/imdb/imdb/reference.md @@ -42,7 +42,6 @@ async fn main() { &CreateMovieRequest { title: "title".to_string(), rating: 1.1, - ..Default::default() }, None, ) @@ -54,6 +53,29 @@ async fn main() { +#### ⚙️ Parameters + +
+
+ +
+
+ +**title:** `String` + +
+
+ +
+
+ +**rating:** `f64` + +
+
+
+
+ diff --git a/seed/rust-sdk/imdb/imdb/src/api/mod.rs b/seed/rust-sdk/imdb/imdb/src/api/mod.rs index c1870d0f50e7..9999104a464b 100644 --- a/seed/rust-sdk/imdb/imdb/src/api/mod.rs +++ b/seed/rust-sdk/imdb/imdb/src/api/mod.rs @@ -1,4 +1,4 @@ -//! API client and types for the Api +//! API client and types for the api //! //! This module contains all the API definitions including request/response types //! and client implementations for interacting with the API. diff --git a/seed/rust-sdk/imdb/imdb/src/api/resources/imdb/imdb.rs b/seed/rust-sdk/imdb/imdb/src/api/resources/imdb/imdb.rs index 30d111e14f60..6439dd3910ca 100644 --- a/seed/rust-sdk/imdb/imdb/src/api/resources/imdb/imdb.rs +++ b/seed/rust-sdk/imdb/imdb/src/api/resources/imdb/imdb.rs @@ -30,7 +30,7 @@ impl ImdbClient { self.http_client .execute_request( Method::POST, - "/movies/create-movie", + "movies/create-movie", Some(serde_json::to_value(request).map_err(ApiError::Serialization)?), None, options, @@ -46,7 +46,7 @@ impl ImdbClient { self.http_client .execute_request( Method::GET, - &format!("/movies/{}", movie_id.0), + &format!("movies/{}", movie_id.0), None, None, options, diff --git a/seed/rust-sdk/imdb/imdb/src/api/types/imdb_create_movie_request.rs b/seed/rust-sdk/imdb/imdb/src/api/types/create_movie_request.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb/src/api/types/imdb_create_movie_request.rs rename to seed/rust-sdk/imdb/imdb/src/api/types/create_movie_request.rs diff --git a/seed/rust-sdk/imdb/imdb/src/api/types/mod.rs b/seed/rust-sdk/imdb/imdb/src/api/types/mod.rs index f78038c8deaa..e13ee0fdcdac 100644 --- a/seed/rust-sdk/imdb/imdb/src/api/types/mod.rs +++ b/seed/rust-sdk/imdb/imdb/src/api/types/mod.rs @@ -1,7 +1,7 @@ -pub mod imdb_create_movie_request; -pub mod imdb_movie; -pub mod imdb_movie_id; +pub mod create_movie_request; +pub mod movie; +pub mod movie_id; -pub use imdb_create_movie_request::CreateMovieRequest; -pub use imdb_movie::Movie; -pub use imdb_movie_id::MovieId; +pub use create_movie_request::CreateMovieRequest; +pub use movie::Movie; +pub use movie_id::MovieId; diff --git a/seed/rust-sdk/imdb/imdb/src/api/types/imdb_movie.rs b/seed/rust-sdk/imdb/imdb/src/api/types/movie.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb/src/api/types/imdb_movie.rs rename to seed/rust-sdk/imdb/imdb/src/api/types/movie.rs diff --git a/seed/rust-sdk/imdb/imdb/src/api/types/imdb_movie_id.rs b/seed/rust-sdk/imdb/imdb/src/api/types/movie_id.rs similarity index 100% rename from seed/rust-sdk/imdb/imdb/src/api/types/imdb_movie_id.rs rename to seed/rust-sdk/imdb/imdb/src/api/types/movie_id.rs diff --git a/seed/rust-sdk/imdb/imdb/src/error.rs b/seed/rust-sdk/imdb/imdb/src/error.rs index d953af0246f6..621cc2bcd786 100644 --- a/seed/rust-sdk/imdb/imdb/src/error.rs +++ b/seed/rust-sdk/imdb/imdb/src/error.rs @@ -2,8 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ApiError { - #[error("MovieDoesNotExistError: Resource not found - {{message}}")] - MovieDoesNotExistError { + #[error("NotFoundError: Resource not found - {{message}}")] + NotFoundError { message: String, resource_id: Option, resource_type: Option, @@ -32,10 +32,10 @@ impl ApiError { pub fn from_response(status_code: u16, body: Option<&str>) -> Self { match status_code { 404 => { - // Parse error body for MovieDoesNotExistError; + // Parse error body for NotFoundError; if let Some(body_str) = body { if let Ok(parsed) = serde_json::from_str::(body_str) { - return Self::MovieDoesNotExistError { + return Self::NotFoundError { message: parsed .get("message") .and_then(|v| v.as_str()) @@ -50,7 +50,7 @@ impl ApiError { }; } } - return Self::MovieDoesNotExistError { + return Self::NotFoundError { message: body.unwrap_or("Unknown error").to_string(), resource_id: None, resource_type: None, diff --git a/seed/rust-sdk/imdb/imdb/src/lib.rs b/seed/rust-sdk/imdb/imdb/src/lib.rs index b55485610f13..72d1f9c1851b 100644 --- a/seed/rust-sdk/imdb/imdb/src/lib.rs +++ b/seed/rust-sdk/imdb/imdb/src/lib.rs @@ -1,6 +1,6 @@ -//! # Api SDK +//! # api SDK //! -//! The official Rust SDK for the Api. +//! The official Rust SDK for the api. //! //! ## Getting Started //! @@ -20,7 +20,6 @@ //! &CreateMovieRequest { //! title: "title".to_string(), //! rating: 1.1, -//! ..Default::default() //! }, //! None, //! ) diff --git a/seed/swift-sdk/imdb/README.md b/seed/swift-sdk/imdb/README.md index c8448d8ca584..5ebee2808b72 100644 --- a/seed/swift-sdk/imdb/README.md +++ b/seed/swift-sdk/imdb/README.md @@ -12,6 +12,7 @@ The Seed Swift library provides convenient access to the Seed APIs from Swift. - [Reference](#reference) - [Usage](#usage) - [Errors](#errors) +- [Request Types](#request-types) - [Advanced](#advanced) - [Additional Headers](#additional-headers) - [Additional Query String Parameters](#additional-query-string-parameters) @@ -53,7 +54,7 @@ import Api private func main() async throws { let client = ApiClient(token: "") - _ = try await client.imdb.createMovie(request: CreateMovieRequest( + _ = try await client.imdb.createMovie(request: .init( title: "title", rating: 1.1 )) @@ -92,6 +93,18 @@ do { } ``` +## Request Types + +The SDK exports all request types as Swift structs. Simply import the SDK module to access them: + +```swift +import Api + +let request = Requests.CreateMovieRequest( + ... +) +``` + ## Advanced ### Additional Headers diff --git a/seed/swift-sdk/imdb/Snippets/Example0.swift b/seed/swift-sdk/imdb/Snippets/Example0.swift index 97ad6e022c5b..dfcf4cbc382b 100644 --- a/seed/swift-sdk/imdb/Snippets/Example0.swift +++ b/seed/swift-sdk/imdb/Snippets/Example0.swift @@ -7,7 +7,7 @@ private func main() async throws { token: "" ) - _ = try await client.imdb.createMovie(request: CreateMovieRequest( + _ = try await client.imdb.createMovie(request: .init( title: "title", rating: 1.1 )) diff --git a/seed/swift-sdk/imdb/Snippets/Example1.swift b/seed/swift-sdk/imdb/Snippets/Example1.swift index 2cb28b8901d8..dfcf4cbc382b 100644 --- a/seed/swift-sdk/imdb/Snippets/Example1.swift +++ b/seed/swift-sdk/imdb/Snippets/Example1.swift @@ -7,7 +7,10 @@ private func main() async throws { token: "" ) - _ = try await client.imdb.getMovie(movieId: "movieId") + _ = try await client.imdb.createMovie(request: .init( + title: "title", + rating: 1.1 + )) } try await main() diff --git a/seed/swift-sdk/imdb/Snippets/Example3.swift b/seed/swift-sdk/imdb/Snippets/Example3.swift new file mode 100644 index 000000000000..2cb28b8901d8 --- /dev/null +++ b/seed/swift-sdk/imdb/Snippets/Example3.swift @@ -0,0 +1,13 @@ +import Foundation +import Api + +private func main() async throws { + let client = ApiClient( + baseURL: "https://api.fern.com", + token: "" + ) + + _ = try await client.imdb.getMovie(movieId: "movieId") +} + +try await main() diff --git a/seed/swift-sdk/imdb/Snippets/Example4.swift b/seed/swift-sdk/imdb/Snippets/Example4.swift new file mode 100644 index 000000000000..2cb28b8901d8 --- /dev/null +++ b/seed/swift-sdk/imdb/Snippets/Example4.swift @@ -0,0 +1,13 @@ +import Foundation +import Api + +private func main() async throws { + let client = ApiClient( + baseURL: "https://api.fern.com", + token: "" + ) + + _ = try await client.imdb.getMovie(movieId: "movieId") +} + +try await main() diff --git a/seed/swift-sdk/imdb/Sources/Requests/Requests+CreateMovieRequest.swift b/seed/swift-sdk/imdb/Sources/Requests/Requests+CreateMovieRequest.swift new file mode 100644 index 000000000000..60f745245297 --- /dev/null +++ b/seed/swift-sdk/imdb/Sources/Requests/Requests+CreateMovieRequest.swift @@ -0,0 +1,40 @@ +import Foundation + +extension Requests { + public struct CreateMovieRequest: Codable, Hashable, Sendable { + public let title: String + public let rating: Double + /// Additional properties that are not explicitly defined in the schema + public let additionalProperties: [String: JSONValue] + + public init( + title: String, + rating: Double, + additionalProperties: [String: JSONValue] = .init() + ) { + self.title = title + self.rating = rating + self.additionalProperties = additionalProperties + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.title = try container.decode(String.self, forKey: .title) + self.rating = try container.decode(Double.self, forKey: .rating) + self.additionalProperties = try decoder.decodeAdditionalProperties(using: CodingKeys.self) + } + + public func encode(to encoder: Encoder) throws -> Void { + var container = encoder.container(keyedBy: CodingKeys.self) + try encoder.encodeAdditionalProperties(self.additionalProperties) + try container.encode(self.title, forKey: .title) + try container.encode(self.rating, forKey: .rating) + } + + /// Keys for encoding/decoding struct properties. + enum CodingKeys: String, CodingKey, CaseIterable { + case title + case rating + } + } +} \ No newline at end of file diff --git a/seed/swift-sdk/imdb/Sources/Resources/Imdb/ImdbClient.swift b/seed/swift-sdk/imdb/Sources/Resources/Imdb/ImdbClient.swift index eff610a16bad..e7c7f14bfb95 100644 --- a/seed/swift-sdk/imdb/Sources/Resources/Imdb/ImdbClient.swift +++ b/seed/swift-sdk/imdb/Sources/Resources/Imdb/ImdbClient.swift @@ -10,7 +10,7 @@ public final class ImdbClient: Sendable { /// Add a movie to the database using the movies/* /... path. /// /// - Parameter requestOptions: Additional options for configuring the request, such as custom headers or timeout settings. - public func createMovie(request: CreateMovieRequest, requestOptions: RequestOptions? = nil) async throws -> MovieId { + public func createMovie(request: Requests.CreateMovieRequest, requestOptions: RequestOptions? = nil) async throws -> MovieId { return try await httpClient.performRequest( method: .post, path: "/movies/create-movie", diff --git a/seed/swift-sdk/imdb/Sources/Schemas/CreateMovieRequest.swift b/seed/swift-sdk/imdb/Sources/Schemas/CreateMovieRequest.swift deleted file mode 100644 index 6fb1b3fec5ed..000000000000 --- a/seed/swift-sdk/imdb/Sources/Schemas/CreateMovieRequest.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -public struct CreateMovieRequest: Codable, Hashable, Sendable { - public let title: String - public let rating: Double - /// Additional properties that are not explicitly defined in the schema - public let additionalProperties: [String: JSONValue] - - public init( - title: String, - rating: Double, - additionalProperties: [String: JSONValue] = .init() - ) { - self.title = title - self.rating = rating - self.additionalProperties = additionalProperties - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.title = try container.decode(String.self, forKey: .title) - self.rating = try container.decode(Double.self, forKey: .rating) - self.additionalProperties = try decoder.decodeAdditionalProperties(using: CodingKeys.self) - } - - public func encode(to encoder: Encoder) throws -> Void { - var container = encoder.container(keyedBy: CodingKeys.self) - try encoder.encodeAdditionalProperties(self.additionalProperties) - try container.encode(self.title, forKey: .title) - try container.encode(self.rating, forKey: .rating) - } - - /// Keys for encoding/decoding struct properties. - enum CodingKeys: String, CodingKey, CaseIterable { - case title - case rating - } -} \ No newline at end of file diff --git a/seed/swift-sdk/imdb/Tests/Core/ClientErrorTests.swift b/seed/swift-sdk/imdb/Tests/Core/ClientErrorTests.swift index 992e1862856b..fdf222646011 100644 --- a/seed/swift-sdk/imdb/Tests/Core/ClientErrorTests.swift +++ b/seed/swift-sdk/imdb/Tests/Core/ClientErrorTests.swift @@ -21,7 +21,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -58,7 +58,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -95,7 +95,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -134,7 +134,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -171,7 +171,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -210,7 +210,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -247,7 +247,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), diff --git a/seed/swift-sdk/imdb/Tests/Core/ClientRetryTests.swift b/seed/swift-sdk/imdb/Tests/Core/ClientRetryTests.swift index 2c953d98b9c9..c0f153dca15a 100644 --- a/seed/swift-sdk/imdb/Tests/Core/ClientRetryTests.swift +++ b/seed/swift-sdk/imdb/Tests/Core/ClientRetryTests.swift @@ -22,7 +22,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -53,7 +53,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -84,7 +84,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -114,7 +114,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -143,7 +143,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -173,7 +173,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -203,7 +203,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -238,7 +238,7 @@ import Testing let startTime = Date() do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -283,7 +283,7 @@ import Testing let startTime = Date() do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -324,7 +324,7 @@ import Testing let startTime = Date() do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -376,7 +376,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -402,7 +402,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), @@ -432,7 +432,7 @@ import Testing do { _ = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( title: "title", rating: 1.1 ), diff --git a/seed/swift-sdk/imdb/Tests/Wire/Resources/Imdb/ImdbClientWireTests.swift b/seed/swift-sdk/imdb/Tests/Wire/Resources/Imdb/ImdbClientWireTests.swift index 33f31701d52d..f5e91816d6ec 100644 --- a/seed/swift-sdk/imdb/Tests/Wire/Resources/Imdb/ImdbClientWireTests.swift +++ b/seed/swift-sdk/imdb/Tests/Wire/Resources/Imdb/ImdbClientWireTests.swift @@ -19,7 +19,32 @@ import Api ) let expectedResponse = "string" let response = try await client.imdb.createMovie( - request: CreateMovieRequest( + request: .init( + title: "title", + rating: 1.1 + ), + requestOptions: RequestOptions(additionalHeaders: stub.headers) + ) + try #require(response == expectedResponse) + } + + @Test func createMovie2() async throws -> Void { + let stub = HTTPStub() + stub.setResponse( + body: Data( + #""" + string + """#.utf8 + ) + ) + let client = ApiClient( + baseURL: "https://api.fern.com", + token: "", + urlSession: stub.urlSession + ) + let expectedResponse = "string" + let response = try await client.imdb.createMovie( + request: .init( title: "title", rating: 1.1 ), @@ -57,4 +82,34 @@ import Api ) try #require(response == expectedResponse) } + + @Test func getMovie2() async throws -> Void { + let stub = HTTPStub() + stub.setResponse( + body: Data( + #""" + { + "id": "id", + "title": "title", + "rating": 1.1 + } + """#.utf8 + ) + ) + let client = ApiClient( + baseURL: "https://api.fern.com", + token: "", + urlSession: stub.urlSession + ) + let expectedResponse = Movie( + id: "id", + title: "title", + rating: 1.1 + ) + let response = try await client.imdb.getMovie( + movieId: "movieId", + requestOptions: RequestOptions(additionalHeaders: stub.headers) + ) + try #require(response == expectedResponse) + } } \ No newline at end of file diff --git a/seed/swift-sdk/imdb/reference.md b/seed/swift-sdk/imdb/reference.md index 9ba6ff32d49c..6ca7ae6934e5 100644 --- a/seed/swift-sdk/imdb/reference.md +++ b/seed/swift-sdk/imdb/reference.md @@ -1,6 +1,6 @@ # Reference ## Imdb -
client.imdb.createMovie(request: CreateMovieRequest, requestOptions: RequestOptions?) -> MovieId +
client.imdb.createMovie(request: Requests.CreateMovieRequest, requestOptions: RequestOptions?) -> MovieId
@@ -33,7 +33,7 @@ import Api private func main() async throws { let client = ApiClient(token: "") - _ = try await client.imdb.createMovie(request: CreateMovieRequest( + _ = try await client.imdb.createMovie(request: .init( title: "title", rating: 1.1 )) @@ -54,7 +54,7 @@ try await main()
-**request:** `CreateMovieRequest` +**request:** `Requests.CreateMovieRequest`
diff --git a/seed/ts-sdk/imdb/branded-string-aliases/README.md b/seed/ts-sdk/imdb/branded-string-aliases/README.md index d2def82e6471..748cd6241616 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/README.md +++ b/seed/ts-sdk/imdb/branded-string-aliases/README.md @@ -10,6 +10,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Installation](#installation) - [Reference](#reference) - [Usage](#usage) +- [Request and Response Types](#request-and-response-types) - [Exception Handling](#exception-handling) - [Advanced](#advanced) - [Subpackage Exports](#subpackage-exports) @@ -48,6 +49,19 @@ await client.imdb.createMovie({ }); ``` +## Request and Response Types + +The SDK exports all request and response types as TypeScript interfaces. Simply import them with the +following namespace: + +```typescript +import { SeedApi } from "@fern/imdb"; + +const request: SeedApi.CreateMovieRequest = { + ... +}; +``` + ## Exception Handling When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error diff --git a/seed/ts-sdk/imdb/branded-string-aliases/reference.md b/seed/ts-sdk/imdb/branded-string-aliases/reference.md index d2344bcf031a..b9930e0a2aa9 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/reference.md +++ b/seed/ts-sdk/imdb/branded-string-aliases/reference.md @@ -66,7 +66,7 @@ await client.imdb.createMovie({
-
client.imdb.getMovie(movieId) -> SeedApi.Movie +
client.imdb.getMovie({ ...params }) -> SeedApi.Movie
@@ -79,7 +79,9 @@ await client.imdb.createMovie({
```typescript -await client.imdb.getMovie(SeedApi.MovieId("movieId")); +await client.imdb.getMovie({ + movieId: SeedApi.MovieId("movieId") +}); ```
@@ -95,7 +97,7 @@ await client.imdb.getMovie(SeedApi.MovieId("movieId"));
-**movieId:** `SeedApi.MovieId` +**request:** `SeedApi.GetMovieImdbRequest`
diff --git a/seed/ts-sdk/imdb/branded-string-aliases/snippet.json b/seed/ts-sdk/imdb/branded-string-aliases/snippet.json index 8069c2d223ef..140d17cdb8fd 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/snippet.json +++ b/seed/ts-sdk/imdb/branded-string-aliases/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedApi, SeedApiClient } from \"@fern/imdb\";\n\nconst client = new SeedApiClient({ environment: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\" });\nawait client.imdb.getMovie(SeedApi.MovieId(\"movieId\"));\n" + "client": "import { SeedApi, SeedApiClient } from \"@fern/imdb\";\n\nconst client = new SeedApiClient({ environment: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\" });\nawait client.imdb.getMovie({\n movieId: SeedApi.MovieId(\"movieId\")\n});\n" } } ], diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/errors/MovieDoesNotExistError.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/errors/NotFoundError.ts similarity index 63% rename from seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/errors/MovieDoesNotExistError.ts rename to seed/ts-sdk/imdb/branded-string-aliases/src/api/errors/NotFoundError.ts index c9ce7e8a2949..ffc8a6adaa21 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/errors/MovieDoesNotExistError.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/errors/NotFoundError.ts @@ -1,13 +1,13 @@ // This file was auto-generated by Fern from our API Definition. -import type * as core from "../../../../core/index.js"; -import * as errors from "../../../../errors/index.js"; -import type * as SeedApi from "../../../index.js"; +import type * as core from "../../core/index.js"; +import * as errors from "../../errors/index.js"; +import type * as SeedApi from "../index.js"; -export class MovieDoesNotExistError extends errors.SeedApiError { +export class NotFoundError extends errors.SeedApiError { constructor(body: SeedApi.MovieId, rawResponse?: core.RawResponse) { super({ - message: "MovieDoesNotExistError", + message: "NotFoundError", statusCode: 404, body: body, rawResponse: rawResponse, diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/errors/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/errors/index.ts new file mode 100644 index 000000000000..709c2ac17e23 --- /dev/null +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/errors/index.ts @@ -0,0 +1 @@ +export * from "./NotFoundError.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/index.ts index e445af0d831e..6ed44b0bf2f9 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/index.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/index.ts @@ -1 +1,3 @@ +export * from "./errors/index.js"; export * from "./resources/index.js"; +export * from "./types/index.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/Client.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/Client.ts index 9b06dba8db2e..96d76d330177 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/Client.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/Client.ts @@ -51,7 +51,7 @@ export class ImdbClient { url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - "/movies/create-movie", + "movies/create-movie", ), method: "POST", headers: _headers, @@ -83,31 +83,34 @@ export class ImdbClient { /** * @deprecated * - * @param {SeedApi.MovieId} movieId + * @param {SeedApi.GetMovieImdbRequest} request * @param {ImdbClient.RequestOptions} requestOptions - Request-specific configuration. * - * @throws {@link SeedApi.MovieDoesNotExistError} + * @throws {@link SeedApi.NotFoundError} * * @example - * await client.imdb.getMovie(SeedApi.MovieId("movieId")) + * await client.imdb.getMovie({ + * movieId: SeedApi.MovieId("movieId") + * }) */ public getMovie( - movieId: SeedApi.MovieId, + request: SeedApi.GetMovieImdbRequest, requestOptions?: ImdbClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__getMovie(movieId, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__getMovie(request, requestOptions)); } private async __getMovie( - movieId: SeedApi.MovieId, + request: SeedApi.GetMovieImdbRequest, requestOptions?: ImdbClient.RequestOptions, ): Promise> { + const { movieId } = request; const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); const _response = await core.fetcher({ url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - `/movies/${core.url.encodePathParam(movieId)}`, + `movies/${core.url.encodePathParam(movieId)}`, ), method: "GET", headers: _headers, @@ -125,10 +128,7 @@ export class ImdbClient { if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { case 404: - throw new SeedApi.MovieDoesNotExistError( - _response.error.body as SeedApi.MovieId, - _response.rawResponse, - ); + throw new SeedApi.NotFoundError(_response.error.body as SeedApi.MovieId, _response.rawResponse); default: throw new errors.SeedApiError({ statusCode: _response.error.statusCode, diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/index.ts index cb0ff5c3b541..195f9aa8a846 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/index.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/index.ts @@ -1 +1 @@ -export {}; +export * from "./requests/index.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/CreateMovieRequest.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/CreateMovieRequest.ts similarity index 62% rename from seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/CreateMovieRequest.ts rename to seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/CreateMovieRequest.ts index 86bfe80124bb..51e8ee74551e 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/CreateMovieRequest.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/CreateMovieRequest.ts @@ -1,5 +1,12 @@ // This file was auto-generated by Fern from our API Definition. +/** + * @example + * { + * title: "title", + * rating: 1.1 + * } + */ export interface CreateMovieRequest { title: string; rating: number; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts new file mode 100644 index 000000000000..60bb18a24d0c --- /dev/null +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as SeedApi from "../../../../index.js"; + +/** + * @example + * { + * movieId: SeedApi.MovieId("movieId") + * } + */ +export interface GetMovieImdbRequest { + movieId: SeedApi.MovieId; +} diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/index.ts new file mode 100644 index 000000000000..4df09ff09f22 --- /dev/null +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/client/requests/index.ts @@ -0,0 +1,2 @@ +export type { CreateMovieRequest } from "./CreateMovieRequest.js"; +export type { GetMovieImdbRequest } from "./GetMovieImdbRequest.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/errors/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/errors/index.ts deleted file mode 100644 index bc53cd67394c..000000000000 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/errors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MovieDoesNotExistError.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/index.ts index b90a45b3ab4b..914b8c3c7214 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/index.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/index.ts @@ -1,3 +1 @@ export * from "./client/index.js"; -export * from "./errors/index.js"; -export * from "./types/index.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/index.ts index bee73c4720c7..c1711337f053 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/index.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/index.ts @@ -1,3 +1,2 @@ -export * from "./imdb/errors/index.js"; +export * from "./imdb/client/requests/index.js"; export * as imdb from "./imdb/index.js"; -export * from "./imdb/types/index.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/Movie.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/types/Movie.ts similarity index 80% rename from seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/Movie.ts rename to seed/ts-sdk/imdb/branded-string-aliases/src/api/types/Movie.ts index a1a5baf796ac..108719f2d39a 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/Movie.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/types/Movie.ts @@ -1,6 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -import type * as SeedApi from "../../../index.js"; +import type * as SeedApi from "../index.js"; export interface Movie { id: SeedApi.MovieId; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/MovieId.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/types/MovieId.ts similarity index 81% rename from seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/MovieId.ts rename to seed/ts-sdk/imdb/branded-string-aliases/src/api/types/MovieId.ts index 1762b2ad7514..b62b57dca2fa 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/MovieId.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/types/MovieId.ts @@ -1,6 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -import type * as SeedApi from "../../../index.js"; +import type * as SeedApi from "../index.js"; export type MovieId = string & { MovieId: void; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/api/types/index.ts similarity index 58% rename from seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/index.ts rename to seed/ts-sdk/imdb/branded-string-aliases/src/api/types/index.ts index e349b81f13b6..e22c5a9f6e92 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/index.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/api/types/index.ts @@ -1,3 +1,2 @@ -export * from "./CreateMovieRequest.js"; export * from "./Movie.js"; export * from "./MovieId.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/tests/wire/imdb.test.ts b/seed/ts-sdk/imdb/branded-string-aliases/tests/wire/imdb.test.ts index c87e08a29e37..ec8e7bda0ff7 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/tests/wire/imdb.test.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/tests/wire/imdb.test.ts @@ -35,7 +35,9 @@ describe("ImdbClient", () => { server.mockEndpoint().get("/movies/movieId").respondWith().statusCode(200).jsonBody(rawResponseBody).build(); - const response = await client.imdb.getMovie(SeedApi.MovieId("movieId")); + const response = await client.imdb.getMovie({ + movieId: SeedApi.MovieId("movieId"), + }); expect(response).toEqual(rawResponseBody); }); @@ -48,7 +50,9 @@ describe("ImdbClient", () => { server.mockEndpoint().get("/movies/movieId").respondWith().statusCode(404).jsonBody(rawResponseBody).build(); await expect(async () => { - return await client.imdb.getMovie(SeedApi.MovieId("movieId")); - }).rejects.toThrow(SeedApi.MovieDoesNotExistError); + return await client.imdb.getMovie({ + movieId: SeedApi.MovieId("movieId"), + }); + }).rejects.toThrow(SeedApi.NotFoundError); }); }); diff --git a/seed/ts-sdk/imdb/no-custom-config/README.md b/seed/ts-sdk/imdb/no-custom-config/README.md index d2def82e6471..748cd6241616 100644 --- a/seed/ts-sdk/imdb/no-custom-config/README.md +++ b/seed/ts-sdk/imdb/no-custom-config/README.md @@ -10,6 +10,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Installation](#installation) - [Reference](#reference) - [Usage](#usage) +- [Request and Response Types](#request-and-response-types) - [Exception Handling](#exception-handling) - [Advanced](#advanced) - [Subpackage Exports](#subpackage-exports) @@ -48,6 +49,19 @@ await client.imdb.createMovie({ }); ``` +## Request and Response Types + +The SDK exports all request and response types as TypeScript interfaces. Simply import them with the +following namespace: + +```typescript +import { SeedApi } from "@fern/imdb"; + +const request: SeedApi.CreateMovieRequest = { + ... +}; +``` + ## Exception Handling When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error diff --git a/seed/ts-sdk/imdb/no-custom-config/reference.md b/seed/ts-sdk/imdb/no-custom-config/reference.md index d66e8e4144c7..be313ccc404b 100644 --- a/seed/ts-sdk/imdb/no-custom-config/reference.md +++ b/seed/ts-sdk/imdb/no-custom-config/reference.md @@ -66,7 +66,7 @@ await client.imdb.createMovie({
-
client.imdb.getMovie(movieId) -> SeedApi.Movie +
client.imdb.getMovie({ ...params }) -> SeedApi.Movie
@@ -79,7 +79,9 @@ await client.imdb.createMovie({
```typescript -await client.imdb.getMovie("movieId"); +await client.imdb.getMovie({ + movieId: "movieId" +}); ```
@@ -95,7 +97,7 @@ await client.imdb.getMovie("movieId");
-**movieId:** `SeedApi.MovieId` +**request:** `SeedApi.GetMovieImdbRequest`
diff --git a/seed/ts-sdk/imdb/no-custom-config/snippet.json b/seed/ts-sdk/imdb/no-custom-config/snippet.json index abb1f3cc77ef..19904996311e 100644 --- a/seed/ts-sdk/imdb/no-custom-config/snippet.json +++ b/seed/ts-sdk/imdb/no-custom-config/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedApiClient } from \"@fern/imdb\";\n\nconst client = new SeedApiClient({ environment: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\" });\nawait client.imdb.getMovie(\"movieId\");\n" + "client": "import { SeedApiClient } from \"@fern/imdb\";\n\nconst client = new SeedApiClient({ environment: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\" });\nawait client.imdb.getMovie({\n movieId: \"movieId\"\n});\n" } } ], diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/errors/MovieDoesNotExistError.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/errors/NotFoundError.ts similarity index 63% rename from seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/errors/MovieDoesNotExistError.ts rename to seed/ts-sdk/imdb/no-custom-config/src/api/errors/NotFoundError.ts index c9ce7e8a2949..ffc8a6adaa21 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/errors/MovieDoesNotExistError.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/errors/NotFoundError.ts @@ -1,13 +1,13 @@ // This file was auto-generated by Fern from our API Definition. -import type * as core from "../../../../core/index.js"; -import * as errors from "../../../../errors/index.js"; -import type * as SeedApi from "../../../index.js"; +import type * as core from "../../core/index.js"; +import * as errors from "../../errors/index.js"; +import type * as SeedApi from "../index.js"; -export class MovieDoesNotExistError extends errors.SeedApiError { +export class NotFoundError extends errors.SeedApiError { constructor(body: SeedApi.MovieId, rawResponse?: core.RawResponse) { super({ - message: "MovieDoesNotExistError", + message: "NotFoundError", statusCode: 404, body: body, rawResponse: rawResponse, diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/errors/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/errors/index.ts new file mode 100644 index 000000000000..709c2ac17e23 --- /dev/null +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/errors/index.ts @@ -0,0 +1 @@ +export * from "./NotFoundError.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/index.ts index e445af0d831e..6ed44b0bf2f9 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/index.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/index.ts @@ -1 +1,3 @@ +export * from "./errors/index.js"; export * from "./resources/index.js"; +export * from "./types/index.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/Client.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/Client.ts index c68e9f6d99a6..ffa79e9ca7ed 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/Client.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/Client.ts @@ -51,7 +51,7 @@ export class ImdbClient { url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - "/movies/create-movie", + "movies/create-movie", ), method: "POST", headers: _headers, @@ -83,31 +83,34 @@ export class ImdbClient { /** * @deprecated * - * @param {SeedApi.MovieId} movieId + * @param {SeedApi.GetMovieImdbRequest} request * @param {ImdbClient.RequestOptions} requestOptions - Request-specific configuration. * - * @throws {@link SeedApi.MovieDoesNotExistError} + * @throws {@link SeedApi.NotFoundError} * * @example - * await client.imdb.getMovie("movieId") + * await client.imdb.getMovie({ + * movieId: "movieId" + * }) */ public getMovie( - movieId: SeedApi.MovieId, + request: SeedApi.GetMovieImdbRequest, requestOptions?: ImdbClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__getMovie(movieId, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__getMovie(request, requestOptions)); } private async __getMovie( - movieId: SeedApi.MovieId, + request: SeedApi.GetMovieImdbRequest, requestOptions?: ImdbClient.RequestOptions, ): Promise> { + const { movieId } = request; const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); const _response = await core.fetcher({ url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - `/movies/${core.url.encodePathParam(movieId)}`, + `movies/${core.url.encodePathParam(movieId)}`, ), method: "GET", headers: _headers, @@ -125,10 +128,7 @@ export class ImdbClient { if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { case 404: - throw new SeedApi.MovieDoesNotExistError( - _response.error.body as SeedApi.MovieId, - _response.rawResponse, - ); + throw new SeedApi.NotFoundError(_response.error.body as SeedApi.MovieId, _response.rawResponse); default: throw new errors.SeedApiError({ statusCode: _response.error.statusCode, diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/index.ts index cb0ff5c3b541..195f9aa8a846 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/index.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/index.ts @@ -1 +1 @@ -export {}; +export * from "./requests/index.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/CreateMovieRequest.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/CreateMovieRequest.ts similarity index 62% rename from seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/CreateMovieRequest.ts rename to seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/CreateMovieRequest.ts index 86bfe80124bb..51e8ee74551e 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/CreateMovieRequest.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/CreateMovieRequest.ts @@ -1,5 +1,12 @@ // This file was auto-generated by Fern from our API Definition. +/** + * @example + * { + * title: "title", + * rating: 1.1 + * } + */ export interface CreateMovieRequest { title: string; rating: number; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts new file mode 100644 index 000000000000..1fdf7ead569e --- /dev/null +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as SeedApi from "../../../../index.js"; + +/** + * @example + * { + * movieId: "movieId" + * } + */ +export interface GetMovieImdbRequest { + movieId: SeedApi.MovieId; +} diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/index.ts new file mode 100644 index 000000000000..4df09ff09f22 --- /dev/null +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/client/requests/index.ts @@ -0,0 +1,2 @@ +export type { CreateMovieRequest } from "./CreateMovieRequest.js"; +export type { GetMovieImdbRequest } from "./GetMovieImdbRequest.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/errors/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/errors/index.ts deleted file mode 100644 index bc53cd67394c..000000000000 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/errors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MovieDoesNotExistError.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/index.ts index b90a45b3ab4b..914b8c3c7214 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/index.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/index.ts @@ -1,3 +1 @@ export * from "./client/index.js"; -export * from "./errors/index.js"; -export * from "./types/index.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/index.ts index bee73c4720c7..c1711337f053 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/index.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/resources/index.ts @@ -1,3 +1,2 @@ -export * from "./imdb/errors/index.js"; +export * from "./imdb/client/requests/index.js"; export * as imdb from "./imdb/index.js"; -export * from "./imdb/types/index.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/Movie.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/types/Movie.ts similarity index 80% rename from seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/Movie.ts rename to seed/ts-sdk/imdb/no-custom-config/src/api/types/Movie.ts index a1a5baf796ac..108719f2d39a 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/Movie.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/types/Movie.ts @@ -1,6 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -import type * as SeedApi from "../../../index.js"; +import type * as SeedApi from "../index.js"; export interface Movie { id: SeedApi.MovieId; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/MovieId.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/types/MovieId.ts similarity index 100% rename from seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/MovieId.ts rename to seed/ts-sdk/imdb/no-custom-config/src/api/types/MovieId.ts diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/api/types/index.ts similarity index 58% rename from seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/index.ts rename to seed/ts-sdk/imdb/no-custom-config/src/api/types/index.ts index e349b81f13b6..e22c5a9f6e92 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/api/resources/imdb/types/index.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/api/types/index.ts @@ -1,3 +1,2 @@ -export * from "./CreateMovieRequest.js"; export * from "./Movie.js"; export * from "./MovieId.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/tests/wire/imdb.test.ts b/seed/ts-sdk/imdb/no-custom-config/tests/wire/imdb.test.ts index 92b03baf0301..440e746cb545 100644 --- a/seed/ts-sdk/imdb/no-custom-config/tests/wire/imdb.test.ts +++ b/seed/ts-sdk/imdb/no-custom-config/tests/wire/imdb.test.ts @@ -35,7 +35,9 @@ describe("ImdbClient", () => { server.mockEndpoint().get("/movies/movieId").respondWith().statusCode(200).jsonBody(rawResponseBody).build(); - const response = await client.imdb.getMovie("movieId"); + const response = await client.imdb.getMovie({ + movieId: "movieId", + }); expect(response).toEqual(rawResponseBody); }); @@ -48,7 +50,9 @@ describe("ImdbClient", () => { server.mockEndpoint().get("/movies/movieId").respondWith().statusCode(404).jsonBody(rawResponseBody).build(); await expect(async () => { - return await client.imdb.getMovie("movieId"); - }).rejects.toThrow(SeedApi.MovieDoesNotExistError); + return await client.imdb.getMovie({ + movieId: "movieId", + }); + }).rejects.toThrow(SeedApi.NotFoundError); }); }); diff --git a/seed/ts-sdk/imdb/omit-undefined/README.md b/seed/ts-sdk/imdb/omit-undefined/README.md index d2def82e6471..748cd6241616 100644 --- a/seed/ts-sdk/imdb/omit-undefined/README.md +++ b/seed/ts-sdk/imdb/omit-undefined/README.md @@ -10,6 +10,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Installation](#installation) - [Reference](#reference) - [Usage](#usage) +- [Request and Response Types](#request-and-response-types) - [Exception Handling](#exception-handling) - [Advanced](#advanced) - [Subpackage Exports](#subpackage-exports) @@ -48,6 +49,19 @@ await client.imdb.createMovie({ }); ``` +## Request and Response Types + +The SDK exports all request and response types as TypeScript interfaces. Simply import them with the +following namespace: + +```typescript +import { SeedApi } from "@fern/imdb"; + +const request: SeedApi.CreateMovieRequest = { + ... +}; +``` + ## Exception Handling When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error diff --git a/seed/ts-sdk/imdb/omit-undefined/reference.md b/seed/ts-sdk/imdb/omit-undefined/reference.md index d66e8e4144c7..be313ccc404b 100644 --- a/seed/ts-sdk/imdb/omit-undefined/reference.md +++ b/seed/ts-sdk/imdb/omit-undefined/reference.md @@ -66,7 +66,7 @@ await client.imdb.createMovie({
-
client.imdb.getMovie(movieId) -> SeedApi.Movie +
client.imdb.getMovie({ ...params }) -> SeedApi.Movie
@@ -79,7 +79,9 @@ await client.imdb.createMovie({
```typescript -await client.imdb.getMovie("movieId"); +await client.imdb.getMovie({ + movieId: "movieId" +}); ```
@@ -95,7 +97,7 @@ await client.imdb.getMovie("movieId");
-**movieId:** `SeedApi.MovieId` +**request:** `SeedApi.GetMovieImdbRequest`
diff --git a/seed/ts-sdk/imdb/omit-undefined/snippet.json b/seed/ts-sdk/imdb/omit-undefined/snippet.json index abb1f3cc77ef..19904996311e 100644 --- a/seed/ts-sdk/imdb/omit-undefined/snippet.json +++ b/seed/ts-sdk/imdb/omit-undefined/snippet.json @@ -19,7 +19,7 @@ }, "snippet": { "type": "typescript", - "client": "import { SeedApiClient } from \"@fern/imdb\";\n\nconst client = new SeedApiClient({ environment: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\" });\nawait client.imdb.getMovie(\"movieId\");\n" + "client": "import { SeedApiClient } from \"@fern/imdb\";\n\nconst client = new SeedApiClient({ environment: \"YOUR_BASE_URL\", token: \"YOUR_TOKEN\" });\nawait client.imdb.getMovie({\n movieId: \"movieId\"\n});\n" } } ], diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/errors/MovieDoesNotExistError.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/errors/NotFoundError.ts similarity index 63% rename from seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/errors/MovieDoesNotExistError.ts rename to seed/ts-sdk/imdb/omit-undefined/src/api/errors/NotFoundError.ts index c9ce7e8a2949..ffc8a6adaa21 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/errors/MovieDoesNotExistError.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/errors/NotFoundError.ts @@ -1,13 +1,13 @@ // This file was auto-generated by Fern from our API Definition. -import type * as core from "../../../../core/index.js"; -import * as errors from "../../../../errors/index.js"; -import type * as SeedApi from "../../../index.js"; +import type * as core from "../../core/index.js"; +import * as errors from "../../errors/index.js"; +import type * as SeedApi from "../index.js"; -export class MovieDoesNotExistError extends errors.SeedApiError { +export class NotFoundError extends errors.SeedApiError { constructor(body: SeedApi.MovieId, rawResponse?: core.RawResponse) { super({ - message: "MovieDoesNotExistError", + message: "NotFoundError", statusCode: 404, body: body, rawResponse: rawResponse, diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/errors/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/errors/index.ts new file mode 100644 index 000000000000..709c2ac17e23 --- /dev/null +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/errors/index.ts @@ -0,0 +1 @@ +export * from "./NotFoundError.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/index.ts index e445af0d831e..6ed44b0bf2f9 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/index.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/index.ts @@ -1 +1,3 @@ +export * from "./errors/index.js"; export * from "./resources/index.js"; +export * from "./types/index.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/Client.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/Client.ts index c68e9f6d99a6..ffa79e9ca7ed 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/Client.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/Client.ts @@ -51,7 +51,7 @@ export class ImdbClient { url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - "/movies/create-movie", + "movies/create-movie", ), method: "POST", headers: _headers, @@ -83,31 +83,34 @@ export class ImdbClient { /** * @deprecated * - * @param {SeedApi.MovieId} movieId + * @param {SeedApi.GetMovieImdbRequest} request * @param {ImdbClient.RequestOptions} requestOptions - Request-specific configuration. * - * @throws {@link SeedApi.MovieDoesNotExistError} + * @throws {@link SeedApi.NotFoundError} * * @example - * await client.imdb.getMovie("movieId") + * await client.imdb.getMovie({ + * movieId: "movieId" + * }) */ public getMovie( - movieId: SeedApi.MovieId, + request: SeedApi.GetMovieImdbRequest, requestOptions?: ImdbClient.RequestOptions, ): core.HttpResponsePromise { - return core.HttpResponsePromise.fromPromise(this.__getMovie(movieId, requestOptions)); + return core.HttpResponsePromise.fromPromise(this.__getMovie(request, requestOptions)); } private async __getMovie( - movieId: SeedApi.MovieId, + request: SeedApi.GetMovieImdbRequest, requestOptions?: ImdbClient.RequestOptions, ): Promise> { + const { movieId } = request; const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers); const _response = await core.fetcher({ url: core.url.join( (await core.Supplier.get(this._options.baseUrl)) ?? (await core.Supplier.get(this._options.environment)), - `/movies/${core.url.encodePathParam(movieId)}`, + `movies/${core.url.encodePathParam(movieId)}`, ), method: "GET", headers: _headers, @@ -125,10 +128,7 @@ export class ImdbClient { if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { case 404: - throw new SeedApi.MovieDoesNotExistError( - _response.error.body as SeedApi.MovieId, - _response.rawResponse, - ); + throw new SeedApi.NotFoundError(_response.error.body as SeedApi.MovieId, _response.rawResponse); default: throw new errors.SeedApiError({ statusCode: _response.error.statusCode, diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/index.ts index cb0ff5c3b541..195f9aa8a846 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/index.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/index.ts @@ -1 +1 @@ -export {}; +export * from "./requests/index.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/CreateMovieRequest.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/CreateMovieRequest.ts similarity index 62% rename from seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/CreateMovieRequest.ts rename to seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/CreateMovieRequest.ts index 86bfe80124bb..51e8ee74551e 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/CreateMovieRequest.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/CreateMovieRequest.ts @@ -1,5 +1,12 @@ // This file was auto-generated by Fern from our API Definition. +/** + * @example + * { + * title: "title", + * rating: 1.1 + * } + */ export interface CreateMovieRequest { title: string; rating: number; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts new file mode 100644 index 000000000000..1fdf7ead569e --- /dev/null +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/GetMovieImdbRequest.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as SeedApi from "../../../../index.js"; + +/** + * @example + * { + * movieId: "movieId" + * } + */ +export interface GetMovieImdbRequest { + movieId: SeedApi.MovieId; +} diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/index.ts new file mode 100644 index 000000000000..4df09ff09f22 --- /dev/null +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/client/requests/index.ts @@ -0,0 +1,2 @@ +export type { CreateMovieRequest } from "./CreateMovieRequest.js"; +export type { GetMovieImdbRequest } from "./GetMovieImdbRequest.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/errors/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/errors/index.ts deleted file mode 100644 index bc53cd67394c..000000000000 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/errors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MovieDoesNotExistError.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/index.ts index b90a45b3ab4b..914b8c3c7214 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/index.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/index.ts @@ -1,3 +1 @@ export * from "./client/index.js"; -export * from "./errors/index.js"; -export * from "./types/index.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/index.ts index bee73c4720c7..c1711337f053 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/index.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/resources/index.ts @@ -1,3 +1,2 @@ -export * from "./imdb/errors/index.js"; +export * from "./imdb/client/requests/index.js"; export * as imdb from "./imdb/index.js"; -export * from "./imdb/types/index.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/Movie.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/types/Movie.ts similarity index 80% rename from seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/Movie.ts rename to seed/ts-sdk/imdb/omit-undefined/src/api/types/Movie.ts index a1a5baf796ac..108719f2d39a 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/Movie.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/types/Movie.ts @@ -1,6 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -import type * as SeedApi from "../../../index.js"; +import type * as SeedApi from "../index.js"; export interface Movie { id: SeedApi.MovieId; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/MovieId.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/types/MovieId.ts similarity index 100% rename from seed/ts-sdk/imdb/omit-undefined/src/api/resources/imdb/types/MovieId.ts rename to seed/ts-sdk/imdb/omit-undefined/src/api/types/MovieId.ts diff --git a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/api/types/index.ts similarity index 58% rename from seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/index.ts rename to seed/ts-sdk/imdb/omit-undefined/src/api/types/index.ts index e349b81f13b6..e22c5a9f6e92 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/api/resources/imdb/types/index.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/api/types/index.ts @@ -1,3 +1,2 @@ -export * from "./CreateMovieRequest.js"; export * from "./Movie.js"; export * from "./MovieId.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/tests/wire/imdb.test.ts b/seed/ts-sdk/imdb/omit-undefined/tests/wire/imdb.test.ts index 92b03baf0301..440e746cb545 100644 --- a/seed/ts-sdk/imdb/omit-undefined/tests/wire/imdb.test.ts +++ b/seed/ts-sdk/imdb/omit-undefined/tests/wire/imdb.test.ts @@ -35,7 +35,9 @@ describe("ImdbClient", () => { server.mockEndpoint().get("/movies/movieId").respondWith().statusCode(200).jsonBody(rawResponseBody).build(); - const response = await client.imdb.getMovie("movieId"); + const response = await client.imdb.getMovie({ + movieId: "movieId", + }); expect(response).toEqual(rawResponseBody); }); @@ -48,7 +50,9 @@ describe("ImdbClient", () => { server.mockEndpoint().get("/movies/movieId").respondWith().statusCode(404).jsonBody(rawResponseBody).build(); await expect(async () => { - return await client.imdb.getMovie("movieId"); - }).rejects.toThrow(SeedApi.MovieDoesNotExistError); + return await client.imdb.getMovie({ + movieId: "movieId", + }); + }).rejects.toThrow(SeedApi.NotFoundError); }); }); diff --git a/test-definitions/fern/apis/imdb/generators.yml b/test-definitions/fern/apis/imdb/generators.yml index a18c8b4a2b06..5e7c1ed7d2f0 100644 --- a/test-definitions/fern/apis/imdb/generators.yml +++ b/test-definitions/fern/apis/imdb/generators.yml @@ -1,4 +1,8 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +api: + specs: + - openapi: ./openapi.yml + overrides: ./openapi-overrides.yml groups: php-sdk: generators: diff --git a/test-definitions/fern/apis/imdb/openapi-overrides.yml b/test-definitions/fern/apis/imdb/openapi-overrides.yml new file mode 100644 index 000000000000..939c5ca29089 --- /dev/null +++ b/test-definitions/fern/apis/imdb/openapi-overrides.yml @@ -0,0 +1,11 @@ +paths: + /movies/create-movie: + post: + x-fern-sdk-group-name: + - imdb + x-fern-sdk-method-name: createMovie + /movies/{movieId}: + get: + x-fern-sdk-group-name: + - imdb + x-fern-sdk-method-name: getMovie diff --git a/test-definitions/fern/apis/imdb/openapi.yml b/test-definitions/fern/apis/imdb/openapi.yml index def154ef4cd8..1123ed72c22f 100644 --- a/test-definitions/fern/apis/imdb/openapi.yml +++ b/test-definitions/fern/apis/imdb/openapi.yml @@ -1,7 +1,7 @@ -openapi: 3.0.1 +openapi: 3.1.0 info: - title: imdb - version: "" + title: api + version: 0.0.0 paths: /movies/create-movie: post: @@ -11,18 +11,19 @@ paths: - Imdb parameters: [] responses: - "201": - description: "" + '201': + description: Success content: application/json: schema: - $ref: "#/components/schemas/MovieId" + $ref: '#/components/schemas/MovieId' + x-fern-availability: pre-release requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/CreateMovieRequest" + $ref: '#/components/schemas/CreateMovieRequest' /movies/{movieId}: get: operationId: imdb_getMovie @@ -33,20 +34,21 @@ paths: in: path required: true schema: - $ref: "#/components/schemas/MovieId" + $ref: '#/components/schemas/MovieId' responses: - "200": - description: "" + '200': + description: Success content: application/json: schema: - $ref: "#/components/schemas/Movie" - "404": - description: "" + $ref: '#/components/schemas/Movie' + '404': + description: MovieDoesNotExistError content: application/json: schema: - $ref: "#/components/schemas/MovieId" + $ref: '#/components/schemas/MovieId' + x-fern-availability: deprecated components: schemas: MovieId: @@ -57,17 +59,19 @@ components: type: object properties: id: - $ref: "#/components/schemas/MovieId" + $ref: '#/components/schemas/MovieId' title: type: string rating: type: number format: double description: The rating scale is one to five stars + x-fern-availability: deprecated required: - id - title - rating + x-fern-availability: generally-available CreateMovieRequest: title: CreateMovieRequest type: object @@ -81,6 +85,6 @@ components: - title - rating securitySchemes: - BearerAuth: + bearer: type: http scheme: bearer