Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/actions/cached-seed/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,35 @@ runs:
uses: ./.github/actions/install

- name: Log in to Docker Hub
id: docker-login
if: ${{ inputs.dockerhub-username != '' }}
uses: docker/login-action@v3
continue-on-error: true
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-token }}

- name: Retry Docker Hub login
id: docker-login-retry
if: ${{ inputs.dockerhub-username != '' && steps.docker-login.outcome == 'failure' }}
uses: docker/login-action@v3
continue-on-error: true
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-token }}

- name: Docker Hub login status
if: ${{ inputs.dockerhub-username != '' }}
shell: bash
run: |
if [[ "${{ steps.docker-login.outcome }}" == "success" ]]; then
echo "Docker Hub login succeeded on first attempt"
elif [[ "${{ steps.docker-login-retry.outcome }}" == "success" ]]; then
echo "Docker Hub login succeeded on retry"
else
echo "::warning::Docker Hub login failed after 2 attempts — continuing without authentication (may hit rate limits)"
fi

- name: Clear stale Docker image cache
shell: bash
run: rm -rf /tmp/docker-cache
Expand Down
4 changes: 2 additions & 2 deletions generators/go-v2/ast/src/utils/resolveRootImportPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ function getMajorVersionSuffix({ config }: { config: FernGeneratorExec.config.Ge
// prefix, e.g. "v0", "v1", "v2", etc.
function parseMajorVersion({ config }: { config: FernGeneratorExec.config.GeneratorConfig }): string | undefined {
const version = getVersion(config);
if (version == null) {
if (version == null || version === "") {
return undefined;
}
const split = version.split(".");
if (split[0] == null) {
if (split[0] == null || split[0] === "" || split[0] === "v") {
return undefined;
}
const majorVersion = split[0];
Expand Down
72 changes: 60 additions & 12 deletions generators/go-v2/sdk/src/wire-tests/WireTestGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,26 +611,74 @@ export class WireTestGenerator {
endpoint: FernIr.HttpEndpoint;
snippet: string;
}): go.CodeBlock {
// Generate the client constructor directly with WireMockBaseURL instead of parsing from snippet
// The snippet uses the original constructor args (e.g., WithToken), but we need WithBaseURL
// Generate the client constructor with WireMockBaseURL and auth options matching the WireMock matchers.
return go.codeblock((writer) => {
writer.write("client := ");
const arguments_: go.AstNode[] = [
go.invokeFunc({
func: go.typeReference({
name: "WithBaseURL",
importPath: this.context.getOptionImportPath()
}),
arguments_: [go.codeblock("WireMockBaseURL")],
multiline: false
})
];
// Add auth options when the endpoint requires authentication, so that the
// request matches the WireMock stub's header matchers.
if (endpoint.auth) {
for (const scheme of this.context.ir.auth.schemes) {
switch (scheme.type) {
case "bearer":
arguments_.push(
go.invokeFunc({
func: go.typeReference({
name: "WithToken",
importPath: this.context.getOptionImportPath()
}),
arguments_: [go.codeblock('"test-token"')],
multiline: false
})
);
break;
case "basic":
arguments_.push(
go.invokeFunc({
func: go.typeReference({
name: "WithBasicAuth",
importPath: this.context.getOptionImportPath()
}),
arguments_: [go.codeblock('"test-username"'), go.codeblock('"test-password"')],
multiline: false
})
);
break;
case "header": {
const fieldName = scheme.name?.name?.pascalCase?.unsafeName;
if (fieldName) {
arguments_.push(
go.invokeFunc({
func: go.typeReference({
name: `With${fieldName}`,
importPath: this.context.getOptionImportPath()
}),
arguments_: [go.codeblock('"test-value"')],
multiline: false
})
);
}
break;
}
}
}
}
writer.writeNode(
go.invokeFunc({
func: go.typeReference({
name: this.context.getClientConstructorName(),
importPath: this.context.getRootClientImportPath()
}),
arguments_: [
go.invokeFunc({
func: go.typeReference({
name: "WithBaseURL",
importPath: this.context.getOptionImportPath()
}),
arguments_: [go.codeblock("WireMockBaseURL")],
multiline: false
})
],
arguments_,
multiline: true
})
);
Expand Down
21 changes: 21 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.33.5
changelogEntry:
- summary: |
Fix dynamic snippet import paths receiving a spurious `/v` suffix when
the SDK version is empty or invalid (e.g. `""`). The major-version
parser now returns `undefined` for empty and bare-`v` version strings
instead of producing an erroneous `v` suffix.
type: fix
createdAt: "2026-04-08"
irVersion: 61

- version: 1.33.4
changelogEntry:
- summary: |
Fix wire test client construction to include auth options (e.g.
WithToken) when the endpoint requires authentication, so that
requests match WireMock stub header matchers.
type: fix
createdAt: "2026-04-08"
irVersion: 61

- version: 1.33.3
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,10 @@ export class DynamicTypeLiteralMapper {
as?: DynamicTypeLiteralMapper.ConvertedAs;
inUndiscriminatedUnion?: boolean;
}): java.TypeLiteral {
const valueRecord = this.context.getRecord(value) ?? {};
const properties = this.context.associateByWireValue({
parameters: object_.properties,
values: this.context.getRecord(value) ?? {}
values: valueRecord
});
// Add missing required properties with default values to ensure valid staged builder code.
// Java uses type-state staged builders where required fields must be set before build() can
Expand Down Expand Up @@ -449,8 +450,10 @@ export class DynamicTypeLiteralMapper {
// Re-sort all properties (including newly added defaults) to match schema declaration order.
// Java staged builders require method calls in the exact order defined by the schema.
const paramOrderMap = new Map<string, number>();
const declaredWireValues = new Set<string>();
object_.properties.forEach((param, index) => {
paramOrderMap.set(param.name.wireValue, index);
declaredWireValues.add(param.name.wireValue);
});
properties.sort(
(a, b) => (paramOrderMap.get(a.name.wireValue) ?? 0) - (paramOrderMap.get(b.name.wireValue) ?? 0)
Expand Down Expand Up @@ -486,6 +489,19 @@ export class DynamicTypeLiteralMapper {
this.context.errors.unscope();
}
}
// Handle extra properties not in the schema via .additionalProperty() builder
// calls so the serialized output matches the example data.
for (const [key, val] of Object.entries(valueRecord)) {
if (!declaredWireValues.has(key) && val !== undefined) {
const rawValue = this.convertToRawJavaLiteral(val);
if (rawValue != null) {
builderParameters.push({
name: "additionalProperty",
value: java.TypeLiteral.raw(`"${this.escapeJavaString(key)}", ${rawValue}`)
});
}
}
}
return java.TypeLiteral.builder({
classReference: this.context.getJavaClassReferenceFromDeclaration({
declaration: object_.declaration
Expand All @@ -494,6 +510,31 @@ export class DynamicTypeLiteralMapper {
});
}

private convertToRawJavaLiteral(value: unknown): string | null {
if (typeof value === "string") {
return `"${this.escapeJavaString(value)}"`;
}
if (typeof value === "number") {
return value.toString();
}
if (typeof value === "boolean") {
return value.toString();
}
if (value === null) {
return "null";
}
return null;
}

private escapeJavaString(s: string): string {
return s
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\t/g, "\\t")
.replace(/\r/g, "\\r");
}

private getDefaultValueForTypeReference(typeReference: FernIr.dynamic.TypeReference): unknown {
switch (typeReference.type) {
case "primitive":
Expand Down
11 changes: 11 additions & 0 deletions generators/java/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 4.1.2
changelogEntry:
- summary: |
Fix wire test snippets dropping additional properties not declared in the
schema. Types with `@JsonAnySetter` now emit `.additionalProperty()` builder
calls for extra fields present in example data, so the serialized output
matches the expected JSON.
type: fix
createdAt: "2026-04-08"
irVersion: 65

- version: 4.1.1
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,9 @@ export function convertSchemaObject(
// primitive types
if (schema.type === "boolean") {
const literalValue = getExtension<boolean>(schema, FernOpenAPIExtension.BOOLEAN_LITERAL);
const resolvedLiteral = literalValue ?? getSingleBooleanEnumValue(schema, blockConstCoercionToLiteral);
const resolvedLiteral =
literalValue ??
getSingleBooleanEnumValue(schema, blockConstCoercionToLiteral, context.options.coerceEnumsToLiterals);
if (resolvedLiteral != null) {
return wrapLiteral({
nameOverride,
Expand Down Expand Up @@ -1508,15 +1510,21 @@ export function convertSchemaObject(

/**
* Extracts a boolean literal from a single-value enum (e.g. `type: boolean, enum: [true]`).
* Returns undefined if the schema doesn't match or if const-to-literal coercion is blocked.
* Returns undefined if the schema doesn't match, if const-to-literal coercion is blocked,
* or if coerceEnumsToLiterals is false. This is consistent with the string enum path (line ~535)
* which also requires coerceEnumsToLiterals to be true.
*/
function getSingleBooleanEnumValue(
schema: OpenAPIV3.SchemaObject,
blockConstCoercionToLiteral: boolean
blockConstCoercionToLiteral: boolean,
coerceEnumsToLiterals: boolean
): boolean | undefined {
if (blockConstCoercionToLiteral) {
return undefined;
}
if (!coerceEnumsToLiterals) {
return undefined;
}
if (schema.enum != null && schema.enum.length === 1 && typeof schema.enum[0] === "boolean") {
return schema.enum[0] as boolean;
}
Expand Down
Loading
Loading