Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b4ebe34
feat(cli): forward --verify through remote (Fiddle) generation path (…
jsklan May 14, 2026
52b7211
chore(cli): release 5.26.1
github-actions[bot] May 14, 2026
94e8c78
fix(generator-cli): set explicit author/committer on API-created comm…
iamnamananand996 May 14, 2026
390b54f
fix(cli): authenticate Venus calls during local Docker generation (#1…
jsklan May 14, 2026
952aec0
chore(cli): release 5.26.2
github-actions[bot] May 14, 2026
31938d1
fix(docs): prefer stored login token over env var for global theme fe…
aditya-arolkar-swe May 14, 2026
5a8e434
chore(cli): release 5.26.3
github-actions[bot] May 14, 2026
dcb3169
chore(deps): bump @fern-api/generator-cli catalog pin to 0.9.27 (#15914)
tstanmay13 May 14, 2026
f1b8a29
fix(python): escape bare < in changelog to fix MDX parse error (#15910)
Ryan-Amirthan May 14, 2026
c54e15f
fix(changelog): change fern.yml to docs.yml in 5.26.0 changelog entry…
Ryan-Amirthan May 14, 2026
b0dec74
feat(php): add SSE / NDJSON / text streaming support (#15882)
patrickthornton May 14, 2026
2b8f01a
chore(php): release 2.10.0
github-actions[bot] May 14, 2026
72780c8
chore(seed): update all seed snapshots (#15918)
fern-support May 14, 2026
e958c9c
chore(seed): update all seed snapshots (#15919)
fern-support May 14, 2026
569eceb
chore(seed): update all seed snapshots (#15920)
fern-support May 14, 2026
42f728e
chore(seed): migrate imdb fixture to OpenAPI input (#15809)
jsklan May 14, 2026
e2a5426
fix(cli): make docs-validator rule init failures honor configured sev…
fern-support May 14, 2026
7c2174b
chore(cli): release 5.26.4
github-actions[bot] May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<YOUR_API_KEY>"
);

await client.Imdb.GetMovieAsync(
"movie_xyz"
new GetMovieImdbRequest {
MovieID = "movie_xyz"
}
);
"
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -531,9 +532,12 @@ func do() {
"<YOUR_API_KEY>",
),
)
request := &acme.GetMovieImdbRequest{
MovieID: "movie_xyz",
}
client.Imdb.GetMovie(
context.TODO(),
"movie_xyz",
request,
)
}
"
Expand Down Expand Up @@ -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"
)
Expand All @@ -1503,9 +1508,12 @@ func do() {
"<YOUR_API_KEY>",
),
)
request := &acme.GetMovieImdbRequest{
MovieID: "movie_xyz",
}
client.Imdb.GetMovie(
context.TODO(),
"movie_xyz",
request,
)
}
"
Expand Down Expand Up @@ -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"
)
Expand All @@ -2475,9 +2484,12 @@ func do() {
"<YOUR_API_KEY>",
),
)
request := &acme.GetMovieImdbRequest{
MovieID: "movie_xyz",
}
client.Imdb.GetMovie(
context.TODO(),
"movie_xyz",
request,
)
}
"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<YOUR_API_KEY>")
.build();

client.imdb().getMovie("movie_xyz");
client.imdb().getMovie(
"movie_xyz",
GetMovieImdbRequest
.builder()
.build()
);
"
`;

Expand Down
7 changes: 7 additions & 0 deletions generators/php/base/src/AsIs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 39 additions & 0 deletions generators/php/base/src/asIs/Client/JsonStream.Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace <%= namespace%>;

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<T>
*/
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,
);
}
}
32 changes: 32 additions & 0 deletions generators/php/base/src/asIs/Client/SseEvent.Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace <%= namespace%>;

/**
* 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,
) {
}
}
101 changes: 101 additions & 0 deletions generators/php/base/src/asIs/Client/SseStream.Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace <%= namespace%>;

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<T>
*/
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<int, SseEvent<T>>
*/
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",
);
}
}
}
}
Loading
Loading