Skip to content

Commit 8732446

Browse files
Merge pull request #67 from gleanwork/cfreeman/fix-ci-xglean-headers-regeneration
fix: move X-Glean header config to preserved hooks package
2 parents 5ba9a83 + a49b965 commit 8732446

6 files changed

Lines changed: 499 additions & 13 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,17 +1626,21 @@ Glean glean = Glean.builder()
16261626
.build();
16271627
```
16281628

1629-
#### Using SDK Constructor Options
1629+
#### Using GleanBuilder (regen-safe)
16301630

16311631
```java
1632-
Glean glean = Glean.builder()
1632+
import com.glean.api_client.glean_api_client.hooks.GleanBuilder;
1633+
1634+
Glean glean = GleanBuilder.create()
16331635
.apiToken(System.getenv("GLEAN_API_TOKEN"))
16341636
.instance("instance-name")
16351637
.excludeDeprecatedAfter("2026-10-15")
16361638
.includeExperimental(true)
16371639
.build();
16381640
```
16391641

1642+
> **Note:** `GleanBuilder` is preserved across SDK regenerations. Generated builder options may change or be removed by regeneration.
1643+
16401644
### Option Reference
16411645

16421646
| Option | Environment Variable | Type | Description |
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package com.glean.api_client.glean_api_client.hooks;
2+
3+
import com.glean.api_client.glean_api_client.Glean;
4+
import com.glean.api_client.glean_api_client.SDKConfiguration;
5+
import com.glean.api_client.glean_api_client.SecuritySource;
6+
import com.glean.api_client.glean_api_client.utils.HTTPClient;
7+
import com.glean.api_client.glean_api_client.utils.RetryConfig;
8+
9+
import java.lang.reflect.Field;
10+
import java.util.Map;
11+
import java.util.Optional;
12+
13+
/**
14+
* Builder wrapper for creating {@link Glean} instances with custom configuration options.
15+
*
16+
* <p>This builder extends the standard SDK builder with additional configuration options
17+
* for experimental features and deprecation testing that are preserved across SDK regenerations.
18+
*
19+
* <p>Example usage:
20+
* <pre>{@code
21+
* Glean glean = GleanBuilder.create()
22+
* .apiToken("your-api-token")
23+
* .instance("instance-name")
24+
* .excludeDeprecatedAfter("2026-10-15")
25+
* .includeExperimental(true)
26+
* .build();
27+
* }</pre>
28+
*/
29+
public final class GleanBuilder {
30+
31+
private final Glean.Builder delegate;
32+
33+
private Optional<String> excludeDeprecatedAfter = Optional.empty();
34+
private Optional<Boolean> includeExperimental = Optional.empty();
35+
36+
private GleanBuilder() {
37+
this.delegate = Glean.builder();
38+
}
39+
40+
/**
41+
* Creates a new builder instance.
42+
*
43+
* @return a new GleanBuilder
44+
*/
45+
public static GleanBuilder create() {
46+
return new GleanBuilder();
47+
}
48+
49+
/**
50+
* Configures the SDK security to use the provided API token.
51+
*
52+
* @param apiToken The API token to use for all requests.
53+
* @return This builder instance.
54+
*/
55+
public GleanBuilder apiToken(String apiToken) {
56+
delegate.apiToken(apiToken);
57+
return this;
58+
}
59+
60+
/**
61+
* Configures the SDK to use a custom security source.
62+
*
63+
* @param securitySource The security source to use for all requests.
64+
* @return This builder instance.
65+
*/
66+
public GleanBuilder securitySource(SecuritySource securitySource) {
67+
delegate.securitySource(securitySource);
68+
return this;
69+
}
70+
71+
/**
72+
* Allows the default HTTP client to be overridden with a custom implementation.
73+
*
74+
* @param client The HTTP client to use for all requests.
75+
* @return This builder instance.
76+
*/
77+
public GleanBuilder client(HTTPClient client) {
78+
delegate.client(client);
79+
return this;
80+
}
81+
82+
/**
83+
* Overrides the default server URL.
84+
*
85+
* @param serverUrl The server URL to use for all requests.
86+
* @return This builder instance.
87+
*/
88+
public GleanBuilder serverURL(String serverUrl) {
89+
delegate.serverURL(serverUrl);
90+
return this;
91+
}
92+
93+
/**
94+
* Overrides the default server URL with a templated URL populated with the provided parameters.
95+
*
96+
* @param serverUrl The server URL to use for all requests.
97+
* @param params The parameters to use when templating the URL.
98+
* @return This builder instance.
99+
*/
100+
public GleanBuilder serverURL(String serverUrl, Map<String, String> params) {
101+
delegate.serverURL(serverUrl, params);
102+
return this;
103+
}
104+
105+
/**
106+
* Overrides the default server by index.
107+
*
108+
* @param serverIdx The server to use for all requests.
109+
* @return This builder instance.
110+
*/
111+
public GleanBuilder serverIndex(int serverIdx) {
112+
delegate.serverIndex(serverIdx);
113+
return this;
114+
}
115+
116+
/**
117+
* Sets the instance variable for URL substitution.
118+
*
119+
* @param instance The instance name to set.
120+
* @return This builder instance.
121+
*/
122+
public GleanBuilder instance(String instance) {
123+
delegate.instance(instance);
124+
return this;
125+
}
126+
127+
/**
128+
* Overrides the default configuration for retries.
129+
*
130+
* @param retryConfig The retry configuration to use for all requests.
131+
* @return This builder instance.
132+
*/
133+
public GleanBuilder retryConfig(RetryConfig retryConfig) {
134+
delegate.retryConfig(retryConfig);
135+
return this;
136+
}
137+
138+
/**
139+
* Enables debug logging for HTTP requests and responses, including JSON body content.
140+
*
141+
* @param enabled Whether to enable debug logging.
142+
* @return This builder instance.
143+
*/
144+
public GleanBuilder enableHTTPDebugLogging(boolean enabled) {
145+
delegate.enableHTTPDebugLogging(enabled);
146+
return this;
147+
}
148+
149+
/**
150+
* Exclude API endpoints that will be deprecated after this date.
151+
* Use this to test your integration against upcoming deprecations.
152+
*
153+
* <p>More information: <a href="https://developers.glean.com/deprecations/overview">Deprecations Overview</a>
154+
*
155+
* @param excludeDeprecatedAfter date string in YYYY-MM-DD format (e.g., '2026-10-15')
156+
* @return This builder instance.
157+
*/
158+
public GleanBuilder excludeDeprecatedAfter(String excludeDeprecatedAfter) {
159+
this.excludeDeprecatedAfter = Optional.ofNullable(excludeDeprecatedAfter);
160+
return this;
161+
}
162+
163+
/**
164+
* Enable experimental API features that are not yet generally available.
165+
* Use this to preview and test new functionality.
166+
*
167+
* <p><strong>Warning:</strong> Experimental features may change or be removed without notice.
168+
* Do not rely on experimental features in production environments.
169+
*
170+
* @param includeExperimental whether to include experimental features
171+
* @return This builder instance.
172+
*/
173+
public GleanBuilder includeExperimental(boolean includeExperimental) {
174+
this.includeExperimental = Optional.of(includeExperimental);
175+
return this;
176+
}
177+
178+
/**
179+
* Builds a new instance of the Glean SDK.
180+
*
181+
* @return The configured Glean instance.
182+
*/
183+
public Glean build() {
184+
Glean sdk = delegate.build();
185+
SDKConfiguration sdkConfiguration = extractSdkConfiguration(sdk);
186+
if (sdkConfiguration != null) {
187+
GleanCustomConfigRegistry.put(
188+
sdkConfiguration,
189+
new GleanCustomConfig(excludeDeprecatedAfter, includeExperimental)
190+
);
191+
}
192+
return sdk;
193+
}
194+
195+
private static SDKConfiguration extractSdkConfiguration(Glean sdk) {
196+
if (sdk == null) {
197+
return null;
198+
}
199+
200+
try {
201+
// Preferred: reflectively access Glean.client -> Client.sdkConfiguration
202+
Object client = readFieldValue(sdk, "client");
203+
if (client != null) {
204+
Object cfg = readFieldValue(client, "sdkConfiguration");
205+
if (cfg instanceof SDKConfiguration) {
206+
return (SDKConfiguration) cfg;
207+
}
208+
}
209+
} catch (RuntimeException e) {
210+
// Best-effort: fall through to generic field scan
211+
}
212+
213+
// Fallback: scan first-level fields for an SDKConfiguration
214+
for (Field f : sdk.getClass().getDeclaredFields()) {
215+
if (!SDKConfiguration.class.isAssignableFrom(f.getType())) {
216+
continue;
217+
}
218+
try {
219+
f.setAccessible(true);
220+
Object cfg = f.get(sdk);
221+
if (cfg instanceof SDKConfiguration) {
222+
return (SDKConfiguration) cfg;
223+
}
224+
} catch (IllegalAccessException e) {
225+
// ignore
226+
}
227+
}
228+
229+
return null;
230+
}
231+
232+
private static Object readFieldValue(Object target, String fieldName) {
233+
Class<?> c = target.getClass();
234+
while (c != null) {
235+
try {
236+
Field f = c.getDeclaredField(fieldName);
237+
f.setAccessible(true);
238+
return f.get(target);
239+
} catch (NoSuchFieldException e) {
240+
c = c.getSuperclass();
241+
} catch (IllegalAccessException e) {
242+
throw new RuntimeException(e);
243+
}
244+
}
245+
return null;
246+
}
247+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.glean.api_client.glean_api_client.hooks;
2+
3+
import java.util.Optional;
4+
5+
/**
6+
* Immutable custom Glean configuration values.
7+
*
8+
* <p>This class holds configuration that is not part of the auto-generated SDK,
9+
* providing a way to configure custom headers and features without modifying
10+
* generated code.
11+
*
12+
* <p>Values are associated with a specific SDK instance via {@link GleanCustomConfigRegistry}.
13+
*/
14+
public final class GleanCustomConfig {
15+
16+
private final Optional<String> excludeDeprecatedAfter;
17+
private final Optional<Boolean> includeExperimental;
18+
19+
public GleanCustomConfig(Optional<String> excludeDeprecatedAfter, Optional<Boolean> includeExperimental) {
20+
this.excludeDeprecatedAfter = excludeDeprecatedAfter != null ? excludeDeprecatedAfter : Optional.empty();
21+
this.includeExperimental = includeExperimental != null ? includeExperimental : Optional.empty();
22+
}
23+
24+
/**
25+
* Gets the date after which deprecated API endpoints should be excluded.
26+
*
27+
* @return Optional containing the date string (YYYY-MM-DD format) if set
28+
*/
29+
public Optional<String> excludeDeprecatedAfter() {
30+
return excludeDeprecatedAfter;
31+
}
32+
33+
/**
34+
* Gets whether experimental API features should be enabled.
35+
*
36+
* @return Optional containing the boolean value if set
37+
*/
38+
public Optional<Boolean> includeExperimental() {
39+
return includeExperimental;
40+
}
41+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.glean.api_client.glean_api_client.hooks;
2+
3+
import com.glean.api_client.glean_api_client.SDKConfiguration;
4+
5+
import java.util.Collections;
6+
import java.util.Map;
7+
import java.util.Optional;
8+
import java.util.WeakHashMap;
9+
10+
/**
11+
* Registry mapping a generated {@link SDKConfiguration} instance to preserved custom configuration.
12+
*
13+
* <p>Speakeasy regenerations overwrite generated classes, so custom configuration is stored outside
14+
* generated code and associated to a specific SDK instance at runtime.
15+
*/
16+
final class GleanCustomConfigRegistry {
17+
18+
private static final Map<SDKConfiguration, GleanCustomConfig> REGISTRY =
19+
Collections.synchronizedMap(new WeakHashMap<>());
20+
21+
private GleanCustomConfigRegistry() {
22+
// prevent instantiation
23+
}
24+
25+
static void put(SDKConfiguration sdkConfiguration, GleanCustomConfig customConfig) {
26+
if (sdkConfiguration == null) {
27+
return;
28+
}
29+
30+
if (customConfig == null) {
31+
return;
32+
}
33+
34+
REGISTRY.put(sdkConfiguration, customConfig);
35+
}
36+
37+
static Optional<GleanCustomConfig> get(SDKConfiguration sdkConfiguration) {
38+
if (sdkConfiguration == null) {
39+
return Optional.empty();
40+
}
41+
return Optional.ofNullable(REGISTRY.get(sdkConfiguration));
42+
}
43+
44+
static void clearForTests() {
45+
REGISTRY.clear();
46+
}
47+
}

0 commit comments

Comments
 (0)