Skip to content

Commit 2a34856

Browse files
author
Steven Yuan
committed
feat(experimentalIdentityAndAuth): update HttpAuthOption and HttpAuthScheme codegen
After upgrading to Smithy 1.37.0 (#906), upgrade to `getEffectiveAuthSchemes` using `NO_AUTH_AWARE` and remove redundant code. For `HttpAuthOption`, allow any auth scheme to generate the corresponding `HttpAuthOption` function regardless of registering it in codegen. For `HttpAuthScheme`, reduce code duplication by looking at "inherited" `LanguageTarget` platforms (e.g. `REACT_NATIVE` inherits from `BROWSER`). `httpAuthSchemes` is only written in a `runtimeConfig` if the `IdentityProvider` and `Signer` are different between platforms, or if the registered `HttpAuthScheme`s are different. `httpAuthSchemes` is always written for `SHARED` as a default. For the `httpAuthSchemes` property on the client config interface, update it to `HttpAuthScheme[]` rather than `IdentityProviderConfiguration`.
1 parent 27a33c8 commit 2a34856

File tree

8 files changed

+264
-156
lines changed

8 files changed

+264
-156
lines changed

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/RuntimeConfigGenerator.java

Lines changed: 164 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@
1616
package software.amazon.smithy.typescript.codegen;
1717

1818
import java.nio.file.Paths;
19+
import java.util.Collections;
1920
import java.util.Iterator;
2021
import java.util.List;
2122
import java.util.Map;
22-
import java.util.Map.Entry;
23+
import java.util.Objects;
2324
import java.util.TreeMap;
2425
import java.util.function.Consumer;
25-
import java.util.stream.Collectors;
2626
import software.amazon.smithy.build.SmithyBuildException;
27+
import software.amazon.smithy.codegen.core.CodegenException;
2728
import software.amazon.smithy.codegen.core.SymbolProvider;
2829
import software.amazon.smithy.model.Model;
2930
import software.amazon.smithy.model.knowledge.ServiceIndex;
3031
import software.amazon.smithy.model.shapes.ServiceShape;
3132
import software.amazon.smithy.model.shapes.ShapeId;
32-
import software.amazon.smithy.model.traits.Trait;
3333
import software.amazon.smithy.typescript.codegen.auth.AuthUtils;
3434
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthScheme;
3535
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthSchemeProviderGenerator;
@@ -208,59 +208,76 @@ private void generateHttpAuthSchemeConfig(
208208
TypeScriptWriter writer,
209209
LanguageTarget target
210210
) {
211+
// feat(experimentalIdentityAndAuth): write the default imported HttpAuthSchemeProvider
212+
if (target.equals(LanguageTarget.SHARED)) {
213+
configs.put("httpAuthSchemeProvider", w -> {
214+
String providerName = "default" + service.toShapeId().getName() + "HttpAuthSchemeProvider";
215+
w.addRelativeImport(providerName, null, Paths.get(".", CodegenUtils.SOURCE_FOLDER,
216+
HttpAuthSchemeProviderGenerator.HTTP_AUTH_FOLDER,
217+
HttpAuthSchemeProviderGenerator.HTTP_AUTH_SCHEME_RESOLVER_MODULE));
218+
w.write(providerName);
219+
});
220+
}
221+
211222
// feat(experimentalIdentityAndAuth): gather HttpAuthSchemes to generate
212-
SupportedHttpAuthSchemesIndex index = new SupportedHttpAuthSchemesIndex(integrations);
223+
SupportedHttpAuthSchemesIndex authIndex = new SupportedHttpAuthSchemesIndex(integrations);
213224
ServiceIndex serviceIndex = ServiceIndex.of(model);
214-
Map<ShapeId, Trait> authSchemeTraits = serviceIndex.getAuthSchemes(service);
215-
Map<ShapeId, HttpAuthScheme> authSchemes = authSchemeTraits.entrySet().stream()
216-
.filter(entry -> index.getHttpAuthScheme(entry.getKey()) != null)
217-
.collect(Collectors.toMap(
218-
entry -> entry.getKey(),
219-
entry -> index.getHttpAuthScheme(entry.getKey())));
220-
// feat(experimentalIdentityAndAuth): can be removed after changing to NO_AUTH_AWARE schemes
221-
// The default set of HttpAuthSchemes is @smithy.api#noAuth on the shared runtime config
222-
authSchemes.put(AuthUtils.NO_AUTH_ID, index.getHttpAuthScheme(AuthUtils.NO_AUTH_ID));
223-
boolean shouldGenerateHttpAuthSchemes = index.getSupportedHttpAuthSchemes().values().stream().anyMatch(value ->
224-
// If either an default identity or signer is configured for the target, generate auth schemes
225-
value.getDefaultIdentityProviders().containsKey(target) || value.getDefaultSigners().containsKey(target));
226-
if (!shouldGenerateHttpAuthSchemes) {
225+
Map<ShapeId, HttpAuthScheme> allEffectiveHttpAuthSchemes =
226+
AuthUtils.getAllEffectiveNoAuthAwareAuthSchemes(service, serviceIndex, authIndex);
227+
List<HttpAuthSchemeTarget> targetAuthSchemes = getHttpAuthSchemeTargets(target, allEffectiveHttpAuthSchemes);
228+
229+
// Generate only if the "inherited" target is different than the current target
230+
List<HttpAuthSchemeTarget> inheritedAuthSchemes = Collections.emptyList();
231+
// Always generated the SHARED target
232+
if (target.equals(LanguageTarget.SHARED)) {
233+
// no-op
234+
// NODE and BROWSER inherit from SHARED
235+
} else if (target.equals(LanguageTarget.NODE) || target.equals(LanguageTarget.BROWSER)) {
236+
inheritedAuthSchemes = getHttpAuthSchemeTargets(LanguageTarget.SHARED, allEffectiveHttpAuthSchemes);
237+
// REACT_NATIVE inherits from BROWSER
238+
} else if (target.equals(LanguageTarget.REACT_NATIVE)) {
239+
inheritedAuthSchemes = getHttpAuthSchemeTargets(LanguageTarget.BROWSER, allEffectiveHttpAuthSchemes);
240+
} else {
241+
throw new CodegenException("Unhandled Language Target: " + target);
242+
}
243+
244+
// If target and inherited auth schemes are equal, then don't generate target auth schemes.
245+
if (targetAuthSchemes.equals(inheritedAuthSchemes)) {
227246
return;
228247
}
229-
// feat(experimentalIdentityAndAuth): write the default imported HttpAuthSchemeProvider
230-
configs.put("httpAuthSchemeProvider", w -> {
231-
String providerName = "default" + service.toShapeId().getName() + "HttpAuthSchemeProvider";
232-
w.addRelativeImport(providerName, null, Paths.get(".", CodegenUtils.SOURCE_FOLDER,
233-
HttpAuthSchemeProviderGenerator.HTTP_AUTH_FOLDER,
234-
HttpAuthSchemeProviderGenerator.HTTP_AUTH_SCHEME_RESOLVER_MODULE));
235-
w.write(providerName);
236-
});
237-
// feat(experimentalIdentityAndAuth): write the default IdentityProviderConfiguration with configured
238-
// HttpAuthSchemes
248+
249+
// feat(experimentalIdentityAndAuth): write the default httpAuthSchemes
239250
configs.put("httpAuthSchemes", w -> {
240251
w.addDependency(TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
241-
w.addImport("DefaultIdentityProviderConfiguration", null,
242-
TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
243-
w.openBlock("new DefaultIdentityProviderConfiguration([", "])", () -> {
244-
Iterator<Entry<ShapeId, HttpAuthScheme>> iter = authSchemes.entrySet().iterator();
252+
w.addImport("IdentityProviderConfig", null, TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
253+
w.openBlock("[", "]", () -> {
254+
Iterator<HttpAuthSchemeTarget> iter = targetAuthSchemes.iterator();
245255
while (iter.hasNext()) {
246-
Entry<ShapeId, HttpAuthScheme> entry = iter.next();
247-
// Default IdentityProvider or HttpSigner, otherwise write {@code never} to force a TypeScript
248-
// compilation failure.
249-
Consumer<TypeScriptWriter> defaultIdentityProvider = entry.getValue()
250-
.getDefaultIdentityProviders()
251-
.getOrDefault(target, AuthUtils.DEFAULT_NEVER_WRITER);
252-
Consumer<TypeScriptWriter> defaultSigner = entry.getValue()
253-
.getDefaultSigners()
254-
.getOrDefault(target, AuthUtils.DEFAULT_NEVER_WRITER);
255-
w.write("""
256-
{
257-
schemeId: $S,
258-
identityProvider: $C,
259-
signer: $C,
260-
}""",
261-
entry.getKey(),
262-
defaultIdentityProvider,
263-
defaultSigner);
256+
HttpAuthSchemeTarget entry = iter.next();
257+
if (entry.identityProvider == null) {
258+
w.write("""
259+
{
260+
schemeId: $S,
261+
identityProvider: (config: IdentityProviderConfig) =>
262+
config.getIdentityProvider($S),
263+
signer: $C,
264+
}""",
265+
entry.httpAuthScheme.getSchemeId(),
266+
entry.httpAuthScheme.getSchemeId(),
267+
entry.signer);
268+
} else {
269+
w.write("""
270+
{
271+
schemeId: $S,
272+
identityProvider: (config: IdentityProviderConfig) =>
273+
config.getIdentityProvider($S) || ($C),
274+
signer: $C,
275+
}""",
276+
entry.httpAuthScheme.getSchemeId(),
277+
entry.httpAuthScheme.getSchemeId(),
278+
entry.identityProvider,
279+
entry.signer);
280+
}
264281
if (iter.hasNext()) {
265282
w.writeInline(", ");
266283
}
@@ -269,6 +286,104 @@ private void generateHttpAuthSchemeConfig(
269286
});
270287
}
271288

289+
private static class HttpAuthSchemeTarget {
290+
public HttpAuthScheme httpAuthScheme;
291+
public Consumer<TypeScriptWriter> identityProvider;
292+
public Consumer<TypeScriptWriter> signer;
293+
294+
HttpAuthSchemeTarget(
295+
HttpAuthScheme httpAuthScheme,
296+
Consumer<TypeScriptWriter> identityProvider,
297+
Consumer<TypeScriptWriter> signer
298+
) {
299+
this.httpAuthScheme = httpAuthScheme;
300+
this.identityProvider = identityProvider;
301+
this.signer = signer;
302+
}
303+
304+
@Override
305+
public boolean equals(Object other) {
306+
if (!(other instanceof HttpAuthSchemeTarget)) {
307+
return false;
308+
}
309+
HttpAuthSchemeTarget o = (HttpAuthSchemeTarget) other;
310+
return Objects.equals(httpAuthScheme, o.httpAuthScheme)
311+
&& Objects.equals(identityProvider, o.identityProvider)
312+
&& Objects.equals(signer, o.signer);
313+
}
314+
315+
@Override
316+
public int hashCode() {
317+
return super.hashCode();
318+
}
319+
}
320+
321+
private List<HttpAuthSchemeTarget> getHttpAuthSchemeTargets(
322+
LanguageTarget target,
323+
Map<ShapeId, HttpAuthScheme> httpAuthSchemes
324+
) {
325+
return getPartialHttpAuthSchemeTargets(target, httpAuthSchemes)
326+
.values()
327+
.stream()
328+
.filter(httpAuthSchemeTarget -> httpAuthSchemeTarget.signer != null)
329+
.toList();
330+
}
331+
332+
private Map<ShapeId, HttpAuthSchemeTarget> getPartialHttpAuthSchemeTargets(
333+
LanguageTarget target,
334+
Map<ShapeId, HttpAuthScheme> httpAuthSchemes
335+
) {
336+
LanguageTarget inherited;
337+
if (target.equals(LanguageTarget.SHARED)) {
338+
// SHARED doesn't inherit any target, so inherited is null
339+
inherited = null;
340+
} else if (target.equals(LanguageTarget.NODE) || target.equals(LanguageTarget.BROWSER)) {
341+
inherited = LanguageTarget.SHARED;
342+
} else if (target.equals(LanguageTarget.REACT_NATIVE)) {
343+
inherited = LanguageTarget.BROWSER;
344+
} else {
345+
throw new CodegenException("Unsupported Language Target: " + target);
346+
}
347+
348+
Map<ShapeId, HttpAuthSchemeTarget> httpAuthSchemeTargets = inherited == null
349+
// SHARED inherits no HttpAuthSchemeTargets
350+
? new TreeMap<>()
351+
// Otherwise, get inherited HttpAuthSchemeTargets
352+
: getPartialHttpAuthSchemeTargets(inherited, httpAuthSchemes);
353+
354+
for (HttpAuthScheme httpAuthScheme : httpAuthSchemes.values()) {
355+
// If HttpAuthScheme is not registered, skip code generation
356+
if (httpAuthScheme == null) {
357+
continue;
358+
}
359+
360+
// Get identity provider and signer for the current target
361+
Consumer<TypeScriptWriter> identityProvider =
362+
httpAuthScheme.getDefaultIdentityProviders().get(target);
363+
Consumer<TypeScriptWriter> signer =
364+
httpAuthScheme.getDefaultSigners().get(target);
365+
366+
HttpAuthSchemeTarget existingEntry =
367+
httpAuthSchemeTargets.get(httpAuthScheme.getSchemeId());
368+
369+
// If HttpAuthScheme is not added yet, add the entry
370+
if (existingEntry == null) {
371+
httpAuthSchemeTargets.put(httpAuthScheme.getSchemeId(),
372+
new HttpAuthSchemeTarget(httpAuthScheme, identityProvider, signer));
373+
continue;
374+
}
375+
376+
// Mutate existing entry for identity provider and signer if available
377+
if (identityProvider != null) {
378+
existingEntry.identityProvider = identityProvider;
379+
}
380+
if (signer != null) {
381+
existingEntry.signer = signer;
382+
}
383+
}
384+
return httpAuthSchemeTargets;
385+
}
386+
272387
private Map<String, Consumer<TypeScriptWriter>> getDefaultRuntimeConfigs(LanguageTarget target) {
273388
switch (target) {
274389
case NODE:

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceBareBonesClientGenerator.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,12 @@ private void generateClientDefaults() {
305305
// feat(experimentalIdentityAndAuth): write httpAuthSchemes and httpAuthSchemeProvider into ClientDefaults
306306
if (settings.getExperimentalIdentityAndAuth()) {
307307
writer.addDependency(TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
308-
writer.addImport("IdentityProviderConfiguration", null,
309-
TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
308+
writer.addImport("HttpAuthScheme", null, TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
310309
writer.writeDocs("""
311310
experimentalIdentityAndAuth: Configuration of HttpAuthSchemes for a client which provides \
312311
default identity providers and signers per auth scheme.
313312
@internal""");
314-
writer.write("httpAuthSchemes?: IdentityProviderConfiguration;\n");
313+
writer.write("httpAuthSchemes?: HttpAuthScheme[];\n");
315314

316315
String httpAuthSchemeProviderName = service.toShapeId().getName() + "HttpAuthSchemeProvider";
317316
writer.addRelativeImport(httpAuthSchemeProviderName, null, Paths.get(".",

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/auth/AuthUtils.java

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@
55

66
package software.amazon.smithy.typescript.codegen.auth;
77

8-
import java.util.function.Consumer;
8+
import java.util.Map;
9+
import java.util.TreeMap;
10+
import software.amazon.smithy.model.knowledge.ServiceIndex;
11+
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
12+
import software.amazon.smithy.model.shapes.ServiceShape;
913
import software.amazon.smithy.model.shapes.ShapeId;
10-
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
14+
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthScheme;
15+
import software.amazon.smithy.typescript.codegen.auth.http.SupportedHttpAuthSchemesIndex;
1116
import software.amazon.smithy.utils.SmithyInternalApi;
1217

1318
/**
1419
* Auth utility methods needed across Java packages.
1520
*/
1621
@SmithyInternalApi
1722
public final class AuthUtils {
18-
/**
19-
* Writes out `never`, which will make TypeScript compilation fail if used as a value.
20-
*/
21-
public static final Consumer<TypeScriptWriter> DEFAULT_NEVER_WRITER = w -> w.write("never");
22-
23-
/**
24-
* Should be replaced when the synthetic NoAuthTrait is released in Smithy.
25-
*/
26-
public static final ShapeId NO_AUTH_ID = ShapeId.from("smithy.api#noAuth");
27-
2823
private AuthUtils() {}
24+
25+
public static Map<ShapeId, HttpAuthScheme> getAllEffectiveNoAuthAwareAuthSchemes(
26+
ServiceShape serviceShape,
27+
ServiceIndex serviceIndex,
28+
SupportedHttpAuthSchemesIndex authIndex
29+
) {
30+
Map<ShapeId, HttpAuthScheme> effectiveAuthSchemes = new TreeMap<>();
31+
var serviceEffectiveAuthSchemes =
32+
serviceIndex.getEffectiveAuthSchemes(serviceShape, AuthSchemeMode.NO_AUTH_AWARE);
33+
for (ShapeId shapeId : serviceEffectiveAuthSchemes.keySet()) {
34+
effectiveAuthSchemes.put(shapeId, authIndex.getHttpAuthScheme(shapeId));
35+
}
36+
for (var operation : serviceShape.getAllOperations()) {
37+
var operationEffectiveAuthSchemes =
38+
serviceIndex.getEffectiveAuthSchemes(serviceShape, operation, AuthSchemeMode.NO_AUTH_AWARE);
39+
for (ShapeId shapeId : operationEffectiveAuthSchemes.keySet()) {
40+
effectiveAuthSchemes.put(shapeId, authIndex.getHttpAuthScheme(shapeId));
41+
}
42+
}
43+
return effectiveAuthSchemes;
44+
}
2945
}

0 commit comments

Comments
 (0)