Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3cb1eca
fix(swift): emit path parameters bound to variables in snippets and w…
devin-ai-integration[bot] Jun 18, 2026
ed7939f
fix(java): support stream-parameter response endpoints (#16577)
devin-ai-integration[bot] Jun 18, 2026
e74f82a
chore(java): release 4.10.3
github-actions[bot] Jun 18, 2026
494bb8c
chore(swift): release 0.35.16
github-actions[bot] Jun 18, 2026
d31ee09
chore(seed): update all seed snapshots (#16623)
thesandlord Jun 18, 2026
018aedf
chore(seed): update all seed snapshots (#16624)
thesandlord Jun 18, 2026
bac8b6e
chore(seed): update all seed snapshots (#16626)
thesandlord Jun 18, 2026
ec63c88
fix(java): wrap optional request body in Optional.of for dynamic snip…
devin-ai-integration[bot] Jun 18, 2026
e7a7d3c
chore(java): release 4.10.4
github-actions[bot] Jun 18, 2026
6a5640a
chore(seed): update all seed snapshots (#16628)
thesandlord Jun 18, 2026
5b74ef4
chore(seed): update all seed snapshots (#16629)
thesandlord Jun 18, 2026
64985ef
fix(cli): retry registerApiDefinition with exponential backoff (#16630)
devin-ai-integration[bot] Jun 18, 2026
95da828
chore(cli): release 5.50.2
github-actions[bot] Jun 18, 2026
1300968
fix(java): respect nullable/optional config flags and fix staged-buil…
devin-ai-integration[bot] Jun 18, 2026
fa15174
chore(java): release 4.10.5
github-actions[bot] Jun 18, 2026
acd7331
fix(java): resolve inline type names in dynamic snippets to nested cl…
devin-ai-integration[bot] Jun 18, 2026
ed1cd9a
chore(java): release 4.10.6
github-actions[bot] Jun 18, 2026
0711cb6
fix(java): collapse nested same-kind optional/nullable in snippet typ…
devin-ai-integration[bot] Jun 18, 2026
fbf7d53
chore(java): release 4.10.7
github-actions[bot] Jun 18, 2026
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
19 changes: 15 additions & 4 deletions generators/java-v2/ast/src/ast/ClassReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,38 @@ export declare namespace ClassReference {
packageName: string;
/* Force the class reference to be fully qualified */
fullyQualified?: boolean;
/**
* The chain of enclosing classes (outermost first) when this is a nested class,
* not including `name`. For example, the nested class `PostRootRequest.Bar` has
* `name: "Bar"` and `enclosingClasses: ["PostRootRequest"]`. Only the outermost
* enclosing class is imported; the reference is written using the dotted path.
*/
enclosingClasses?: string[];
}
}

export class ClassReference extends AstNode {
public readonly name: string;
public readonly packageName: string;
public readonly fullyQualified: boolean;
public readonly enclosingClasses: string[];

constructor({ name, packageName, fullyQualified }: ClassReference.Args) {
constructor({ name, packageName, fullyQualified, enclosingClasses }: ClassReference.Args) {
super();
this.name = name;
this.packageName = packageName;
this.fullyQualified = fullyQualified ?? false;
this.enclosingClasses = enclosingClasses ?? [];
}

public write(writer: Writer): void {
writer.addImport(`${this.packageName}.${this.name}`);
const topLevelClassName = this.enclosingClasses[0] ?? this.name;
const qualifiedName = [...this.enclosingClasses, this.name].join(".");
writer.addImport(`${this.packageName}.${topLevelClassName}`);
if (this.fullyQualified) {
writer.write(`${this.packageName}.${this.name}`);
writer.write(`${this.packageName}.${qualifiedName}`);
return;
}
writer.write(this.name);
writer.write(qualifiedName);
}
}
20 changes: 17 additions & 3 deletions generators/java-v2/ast/src/ast/TypeLiteral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ interface Builder {
export interface BuilderParameter {
name: string;
value: TypeLiteral;
/**
* Whether this property is a required stage in the generated staged builder. When set, it takes
* precedence over inferring required-ness from the emitted value. This matters because a nullable
* property may be emitted as a raw (non-Optional) value yet still belong on the builder's final
* stage rather than the required staged chain.
*/
isRequired?: boolean;
}

interface Bytes {
Expand Down Expand Up @@ -526,7 +533,7 @@ export class TypeLiteral extends AstNode {
}

public orderBuilderParameters(parameters: java.BuilderParameter[]): java.BuilderParameter[] {
const hasRequiredFields = parameters.some((p) => !p.value.isOptional() && !this.isCollection(p.value));
const hasRequiredFields = parameters.some((p) => this.isRequiredBuilderParameter(p));

if (!hasRequiredFields) {
return parameters.sort((a, b) => {
Expand All @@ -545,8 +552,8 @@ export class TypeLiteral extends AstNode {
}

return parameters.sort((a, b) => {
const aIsNonRequired = this.isNonRequired(a.value);
const bIsNonRequired = this.isNonRequired(b.value);
const aIsNonRequired = !this.isRequiredBuilderParameter(a);
const bIsNonRequired = !this.isRequiredBuilderParameter(b);

if (aIsNonRequired && !bIsNonRequired) {
return 1;
Expand All @@ -564,6 +571,13 @@ export class TypeLiteral extends AstNode {
return internalType === "list" || internalType === "set" || internalType === "map";
}

private isRequiredBuilderParameter(parameter: java.BuilderParameter): boolean {
if (parameter.isRequired != null) {
return parameter.isRequired;
}
return !this.isNonRequired(parameter.value);
}

private isNonRequired(value: TypeLiteral): boolean {
return value.isOptional() || this.isCollection(value);
}
Expand Down
75 changes: 75 additions & 0 deletions generators/java-v2/ast/src/ast/__test__/ClassReference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { ClassReference } from "../ClassReference.js";
import { Writer } from "../core/Writer.js";

function makeWriter(): Writer {
return new Writer({ packageName: "com.example", customConfig: {} as never });
}

describe("ClassReference", () => {
describe("without enclosingClasses", () => {
it("writes the simple class name and imports the full package path", () => {
const ref = new ClassReference({ name: "Foo", packageName: "com.example.types" });
const writer = makeWriter();
ref.write(writer);
expect(writer.toString()).toBe("Foo");
expect([...writer.getImports()]).toContain("com.example.types.Foo");
});

it("writes the fully qualified name when fullyQualified is true", () => {
const ref = new ClassReference({ name: "Foo", packageName: "com.example.types", fullyQualified: true });
const writer = makeWriter();
ref.write(writer);
expect(writer.toString()).toBe("com.example.types.Foo");
});
});

describe("with enclosingClasses (nested class)", () => {
it("writes Parent.Child and imports only the outermost class", () => {
const ref = new ClassReference({
name: "Child",
packageName: "com.example.types",
enclosingClasses: ["Parent"]
});
const writer = makeWriter();
ref.write(writer);
// Written name uses dotted path
expect(writer.toString()).toBe("Parent.Child");
// Import is for the top-level class only — NOT Parent.Child
expect([...writer.getImports()]).toContain("com.example.types.Parent");
expect([...writer.getImports()]).not.toContain("com.example.types.Parent.Child");
expect([...writer.getImports()]).not.toContain("com.example.types.Child");
});

it("writes A.B.C and imports only the outermost class for deep nesting", () => {
const ref = new ClassReference({
name: "C",
packageName: "com.example.types",
enclosingClasses: ["A", "B"]
});
const writer = makeWriter();
ref.write(writer);
expect(writer.toString()).toBe("A.B.C");
expect([...writer.getImports()]).toContain("com.example.types.A");
expect([...writer.getImports()]).not.toContain("com.example.types.A.B");
expect([...writer.getImports()]).not.toContain("com.example.types.A.B.C");
});

it("writes the fully qualified dotted path when fullyQualified is true", () => {
const ref = new ClassReference({
name: "Bar",
packageName: "com.example.requests",
enclosingClasses: ["PostRootRequest"],
fullyQualified: true
});
const writer = makeWriter();
ref.write(writer);
expect(writer.toString()).toBe("com.example.requests.PostRootRequest.Bar");
});

it("defaults enclosingClasses to empty array", () => {
const ref = new ClassReference({ name: "Solo", packageName: "com.example" });
expect(ref.enclosingClasses).toEqual([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const BaseJavaCustomConfigSchema = z.object({
"use-default-request-parameter-values": z.boolean().optional(),
"enable-wire-tests": z.boolean().default(false),
"collapse-optional-nullable": z.boolean().optional(),
"use-nullable-annotation": z.boolean().optional(),
"custom-readme-sections": z.array(CustomReadmeSectionSchema).optional(),
"custom-pager-name": z.string().optional(),
"offset-semantics": z.enum(["item-index", "page-index"]).optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ export class DynamicSnippetsGenerator extends AbstractDynamicSnippetsGenerator<
constructor({
ir,
config,
options = {}
options = {},
inlineTypeIds
}: {
ir: FernIr.dynamic.DynamicIntermediateRepresentation;
config: FernGeneratorExec.GeneratorConfig;
options?: Options;
inlineTypeIds?: Set<string>;
}) {
super(new DynamicSnippetsGeneratorContext({ ir, config, options }));
super(new DynamicSnippetsGeneratorContext({ ir, config, options, inlineTypeIds }));
}

public async generate(
Expand Down
52 changes: 42 additions & 10 deletions generators/java-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,18 @@ export class EndpointSnippetGenerator {
if (isCollapsedOptionalNullable) {
return this.context.getOptionalNullableOf(convertedValue);
} else {
// The generated client method accepts the body as a single Optional<T>
// argument and therefore requires an explicit Optional.of(...). When the
// body nests optional/nullable (e.g. optional<nullable<T>>), converting
// body.value.value yields an Optional literal that renders without
// Optional.of(...), and wrapping it again is deduped away. Convert the
// fully unwrapped type so the explicit Optional.of(...) is preserved.
return java.TypeLiteral.optional({
value: convertedValue,
value: this.context.dynamicTypeLiteralMapper.convert({
typeReference: stripOptionalAndNullable(body.value),
value,
as: "request"
}),
useOf: true
});
}
Expand Down Expand Up @@ -855,22 +865,25 @@ export class EndpointSnippetGenerator {
}));
this.context.errors.unscope();

const requestClassReference = java.classReference({
name: this.context.getClassName(request.declaration.name),
packageName: this.context.getRequestsPackageName(request.declaration.fernFilepath)
});

this.context.errors.scope(Scope.RequestBody);
const requestBodyFields =
request.body != null
? this.getInlinedRequestBodyBuilderParameters({
body: request.body,
value: snippet.requestBody,
filePropertyInfo
filePropertyInfo,
enclosing: requestClassReference
})
: [];
this.context.errors.unscope();

return java.TypeLiteral.builder({
classReference: java.classReference({
name: this.context.getClassName(request.declaration.name),
packageName: this.context.getRequestsPackageName(request.declaration.fernFilepath)
}),
classReference: requestClassReference,
parameters: [...pathParameterFields, ...headerFields, ...queryParameterFields, ...requestBodyFields]
});
}
Expand Down Expand Up @@ -933,15 +946,21 @@ export class EndpointSnippetGenerator {
private getInlinedRequestBodyBuilderParameters({
body,
value,
filePropertyInfo
filePropertyInfo,
enclosing
}: {
body: FernIr.dynamic.InlinedRequestBody;
value: unknown;
filePropertyInfo: FilePropertyInfo;
enclosing: java.ClassReference;
}): java.BuilderParameter[] {
switch (body.type) {
case "properties":
return this.getInlinedRequestBodyPropertyBuilderParameters({ parameters: body.value, value });
return this.getInlinedRequestBodyPropertyBuilderParameters({
parameters: body.value,
value,
enclosing
});
case "referenced":
return [this.getReferencedRequestBodyPropertyBuilderParameter({ body, value })];
case "fileUpload":
Expand Down Expand Up @@ -1002,10 +1021,12 @@ export class EndpointSnippetGenerator {

private getInlinedRequestBodyPropertyBuilderParameters({
parameters,
value
value,
enclosing
}: {
parameters: FernIr.dynamic.NamedParameter[];
value: unknown;
enclosing: java.ClassReference;
}): java.BuilderParameter[] {
const bodyProperties = this.context.associateByWireValue({
parameters,
Expand All @@ -1015,12 +1036,23 @@ export class EndpointSnippetGenerator {
(parameter) => !this.context.isDirectLiteral(parameter.typeReference)
);
const sortedProperties = this.context.sortTypeInstancesByRequiredFirst(filteredProperties, parameters);
const reservedNames = new Set<string>([...enclosing.enclosingClasses, enclosing.name]);
const siblingPropertyNames = new Set<string>(
parameters.map((parameter) => this.context.getClassName(parameter.name.name))
);
return sortedProperties.map((parameter) => ({
name: this.context.getMethodName(parameter.name.name),
value: this.context.dynamicTypeLiteralMapper.convert({
typeReference: parameter.typeReference,
value: parameter.value,
as: "request"
as: "request",
nestedClassReference: this.context.dynamicTypeLiteralMapper.resolveInlineNestedClassReference({
enclosing,
baseName: this.context.getClassName(parameter.name.name),
typeReference: parameter.typeReference,
reservedNames,
siblingPropertyNames
})
})
}));
}
Expand Down
Loading
Loading