Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changelog/bugfix-fix-legacy-id-table.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "tools",
"contributor": "",
"description": "Derive generated service client namespaces from the service ID in both the C2J and Smithy code generators. Generated client output is unchanged."
}
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@
"eusc",
"EUSC",
"CBOR",
"cbor"
"cbor",
"dualstack"
],
"ignoreWords": [
// variables defined in ConcurrentStreamBuf
Expand Down
14 changes: 14 additions & 0 deletions tools/code-generation/generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@
</properties>

<build>
<resources>
<!-- Default resources (Velocity templates, etc.) -->
<resource>
<directory>src/main/resources</directory>
</resource>
<!-- Shared service-id -> namespace identifier table, single source of truth shared with
the Smithy generator. Bundled onto the classpath so ServiceNamespaceTable can load it. -->
<resource>
<directory>../shared</directory>
<includes>
<include>service-namespace-table.json</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ public String findFirstSupportedProtocol() {
return protocol;
}

//We're releasing CBOR so current services (only arc-region-switch) need to opt into the new protocol priority
if (serviceFullName != null && serviceFullName.equalsIgnoreCase("ARC - Region switch")){
//We're releasing CBOR so current services (only arc-region-switch) need to opt into the new protocol priority.
//Match on the service id (serviceId), NOT the display name (serviceFullName), so a
//display-name change can never silently flip the wire protocol (JSON <-> CBOR).
if (serviceId != null && serviceId.equalsIgnoreCase("ARC Region switch")){
return "json";
}

return protocols.stream().filter(supportedProtocols::contains)
.min(Comparator.comparingInt(protocolName -> supportedProtocols.indexOf(protocolName)))
.orElseThrow(() -> new RuntimeException(String.format("No supported protocol found for %s", serviceFullName)));
.orElseThrow(() -> new RuntimeException(String.format("No supported protocol found for %s", serviceId)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,63 +52,15 @@ public class C2jModelToGeneratorModelTransformer {
UNSUPPORTEDHTMLTAGS.add("</important>");
}

// A list of non-compliant service models that are already released, so we keep them backward-compatible.
public static final HashSet<String> LEGACY_SERVICE_IDS = new HashSet<>();
// A few ancient models carry no "serviceId" at all, so their namespace cannot be keyed by one
// (identifiers must not be keyed on a display name, endpoint prefix, or model file name). Their
// already-released namespaces are therefore preserved by model (service) name. Every other
// preserved namespace lives in the shared, serviceId-keyed identifier table
// (service-namespace-table.json via ServiceNamespaceTable).
private static final Map<String, String> SERVICE_ID_LESS_NAMESPACES = new HashMap<>();
static {
LEGACY_SERVICE_IDS.add("amp");
LEGACY_SERVICE_IDS.add("appintegrations");
LEGACY_SERVICE_IDS.add("billingconductor");
LEGACY_SERVICE_IDS.add("clouddirectory");
LEGACY_SERVICE_IDS.add("cloudfront");
LEGACY_SERVICE_IDS.add("cloudsearch");
LEGACY_SERVICE_IDS.add("cloudsearchdomain");
LEGACY_SERVICE_IDS.add("codeartifact");
LEGACY_SERVICE_IDS.add("codestar-notifications");
LEGACY_SERVICE_IDS.add("config");
LEGACY_SERVICE_IDS.add("databrew");
LEGACY_SERVICE_IDS.add("ec2");
LEGACY_SERVICE_IDS.add("elasticache");
LEGACY_SERVICE_IDS.add("emr-containers");
LEGACY_SERVICE_IDS.add("entitlement.marketplace");
LEGACY_SERVICE_IDS.add("events");
LEGACY_SERVICE_IDS.add("evidently");
LEGACY_SERVICE_IDS.add("forecast");
LEGACY_SERVICE_IDS.add("forecastquery");
LEGACY_SERVICE_IDS.add("grafana");
LEGACY_SERVICE_IDS.add("importexport");
LEGACY_SERVICE_IDS.add("inspector");
LEGACY_SERVICE_IDS.add("lambda");
LEGACY_SERVICE_IDS.add("location");
LEGACY_SERVICE_IDS.add("lookoutvision");
LEGACY_SERVICE_IDS.add("m2");
LEGACY_SERVICE_IDS.add("migrationhubstrategy");
LEGACY_SERVICE_IDS.add("mobile");
LEGACY_SERVICE_IDS.add("mobileanalytics");
LEGACY_SERVICE_IDS.add("mq");
LEGACY_SERVICE_IDS.add("nimble");
LEGACY_SERVICE_IDS.add("opensearch");
LEGACY_SERVICE_IDS.add("rbin");
LEGACY_SERVICE_IDS.add("rds-data");
LEGACY_SERVICE_IDS.add("redshift-data");
LEGACY_SERVICE_IDS.add("resiliencehub");
LEGACY_SERVICE_IDS.add("rum");
LEGACY_SERVICE_IDS.add("sagemaker-a2i-runtime");
LEGACY_SERVICE_IDS.add("sagemaker-edge");
LEGACY_SERVICE_IDS.add("schemas");
LEGACY_SERVICE_IDS.add("sdb");
LEGACY_SERVICE_IDS.add("transcribe");
LEGACY_SERVICE_IDS.add("transcribe-streaming");
LEGACY_SERVICE_IDS.add("wisdom");

// remaps
LEGACY_SERVICE_IDS.add("lex");
LEGACY_SERVICE_IDS.add("lexv2-runtime");
LEGACY_SERVICE_IDS.add("lexv2-models");
LEGACY_SERVICE_IDS.add("marketplace-entitlement");
LEGACY_SERVICE_IDS.add("sagemaker-runtime");
LEGACY_SERVICE_IDS.add("awstransfer");
LEGACY_SERVICE_IDS.add("transcribestreaming");
LEGACY_SERVICE_IDS.add("dynamodbstreams");
SERVICE_ID_LESS_NAMESPACES.put("importexport", "ImportExport");
SERVICE_ID_LESS_NAMESPACES.put("sdb", "SimpleDB");
}

/**
Expand Down Expand Up @@ -310,30 +262,19 @@ Metadata convertMetadata() {
}
metadata.setProtocols(c2jMetadata.getProtocols());
metadata.setAuth(c2jMetadata.getAuth());
metadata.setNamespace(c2jMetadata.getServiceAbbreviation());
metadata.setServiceFullName(c2jMetadata.getServiceFullName());

@pulimsr pulimsr Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more docgen id read in codegen: Metadata.java#L76 pins arc-region-switch to JSON via serviceFullName.equalsIgnoreCase("ARC - Region switch"). With a placeholder serviceFullName, the client switches from AWSJsonClient to AWSRpcV2CborClient. If this display name ever changes (e.g. in the rebrand), the client would silently switch its wire protocol from JSON to CBOR. Matching on serviceId ("ARC Region switch") would keep this stable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced with serviceId

metadata.setSignatureVersion(c2jMetadata.getSignatureVersion());
metadata.setTargetPrefix(c2jMetadata.getTargetPrefix());
metadata.setGlobalEndpoint(c2jMetadata.getGlobalEndpoint());
metadata.setTimestampFormat(c2jMetadata.getTimestampFormat());

if (metadata.getNamespace() == null || metadata.getNamespace().isEmpty()) {
String serviceId = c2jMetadata.getServiceId();
if(LEGACY_SERVICE_IDS.contains(this.c2jServiceModel.getServiceName())) {
serviceId = c2jMetadata.getServiceFullName(); // fallback to full name for legacy already released models
}
if(serviceId == null || serviceId.isEmpty()) {
throw new SourceGenerationFailedException(String.format("Service model file %s must have metadata.serviceId present", c2jServiceModel.getServiceName()));
}
metadata.setNamespace(sanitizeServiceAbbreviation(serviceId));
} else {
metadata.setNamespace(sanitizeServiceAbbreviation(metadata.getNamespace()));
}
metadata.setServiceId(c2jMetadata.getServiceId() != null ? c2jMetadata.getServiceId() : c2jMetadata.getEndpointPrefix());
// Service namespace is derived from the service id ("serviceId") ONLY. The display-name
// fields (serviceAbbreviation / serviceFullName) MUST NOT be read to derive it. Names that
// diverge from the serviceId-derived name are preserved via the static, serviceId-keyed
// identifier table (ServiceNamespaceTable).
metadata.setNamespace(deriveNamespace(c2jMetadata, this.c2jServiceModel.getServiceName()));

if ("S3-CRT".equalsIgnoreCase(c2jServiceModel.getServiceName())) {
metadata.setNamespace("S3Crt");
}
metadata.setServiceId(c2jMetadata.getServiceId() != null ? c2jMetadata.getServiceId() : c2jMetadata.getEndpointPrefix());

metadata.setClassNamePrefix(CppViewHelper.convertToUpperCamel(ifNotNullOrEmpty(c2jMetadata.getClientClassNamePrefix(), metadata.getNamespace())));

Expand Down Expand Up @@ -1037,6 +978,46 @@ Error convertError(C2jError c2jError, String operationName) {
return error;
}

/**
* Derive the service namespace from the service id ("serviceId") only, consulting the frozen,
* serviceId-keyed identifier table (ServiceNamespaceTable) for already-released names that
* diverge from it.
*
* @param c2jMetadata the raw C2J metadata for the service
* @param serviceModelName the model (service) name, used only for S3-CRT and for the two
* serviceId-less exceptions; never used to derive a tabled value
*/
String deriveNamespace(C2jMetadata c2jMetadata, String serviceModelName) {
// S3-CRT is a projection of s3 with no model of its own.
if ("S3-CRT".equalsIgnoreCase(serviceModelName)) {
return "S3Crt";
}
// Exception: pre-serviceId models (see SERVICE_ID_LESS_NAMESPACES).
String serviceIdLess = SERVICE_ID_LESS_NAMESPACES.get(serviceModelName);
if (serviceIdLess != null) {
return serviceIdLess;
}
String serviceId = c2jMetadata.getServiceId();
if (serviceId == null || serviceId.isEmpty()) {
throw new SourceGenerationFailedException(String.format(
"Service model file %s must have metadata.serviceId present", serviceModelName));
}
// Preserved, already-released name takes precedence over derivation.
String preserved = ServiceNamespaceTable.getNamespace(serviceId);
if (preserved != null) {
return preserved;
}
String derived = sanitizeServiceAbbreviation(serviceId);
// Uniqueness guard: a serviceId-derived name must never collide with a preserved one.
if (ServiceNamespaceTable.isReservedNamespace(derived)) {
throw new SourceGenerationFailedException(String.format(
"Derived namespace '%s' for serviceId '%s' collides with a reserved namespace "
+ "in the identifier table; the serviceId or the table must be reconciled",
derived, serviceId));
}
return derived;
}

String sanitizeServiceAbbreviation(String serviceAbbreviation) {
return serviceAbbreviation.replace(" ", "").replace("-", "").replace("_", "").replace("Amazon", "").replace("AWS", "").replace("/", "");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

package com.amazonaws.util.awsclientgenerator.transform;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;

/**
* Static, checked-in table that preserves the C++ SDK service namespaces that diverge from the
* namespace derived from the service's {@code "serviceId"} (so previously released names stay
* stable).
*
* <p>The table is keyed by the exact, untransformed {@code "serviceId"} and is loaded from the
* shared data file {@code service-namespace-table.json}, which is the single source of truth
* shared with the Smithy generator. Identifiers are derived from {@code "serviceId"} only, never
* from the display-name fields ({@code serviceAbbreviation}/{@code serviceFullName}), the endpoint
* prefix, or the model file name; the values here are frozen literals, not computed from model
* metadata at generation time.
*/
public final class ServiceNamespaceTable {

private static final String RESOURCE = "/service-namespace-table.json";
private static final Map<String, String> TABLE = load();

private ServiceNamespaceTable() {
}

private static Map<String, String> load() {
try (InputStream in = ServiceNamespaceTable.class.getResourceAsStream(RESOURCE)) {
if (in == null) {
throw new IllegalStateException("Missing classpath resource " + RESOURCE);
}
Type mapType = new TypeToken<Map<String, String>>() {
}.getType();
Map<String, String> parsed = new Gson().fromJson(
new InputStreamReader(in, StandardCharsets.UTF_8), mapType);
if (parsed == null) {
throw new IllegalStateException("Empty or invalid " + RESOURCE);
}
return Collections.unmodifiableMap(parsed);
} catch (Exception e) {
throw new IllegalStateException("Failed to load " + RESOURCE, e);
}
}

/**
* @return the frozen namespace preserved for this serviceId, or {@code null} if the service
* is not in the table (in which case the caller derives the namespace from the serviceId).
*/
public static String getNamespace(String serviceId) {
return TABLE.get(serviceId);
}

/**
* @return {@code true} if the given namespace is already reserved by a preserved (tabled)
* service. Used to fail generation when a serviceId-derived name would collide with one.
*/
public static boolean isReservedNamespace(String namespace) {
return TABLE.containsValue(namespace);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,67 @@ public void testMetadataConversationWithStandalonePackages() {
c2jMetadata.setServiceFullName("Amazon AWS Service Abbr Full Name");
c2jMetadata.setSignatureVersion("v4");
c2jMetadata.setTargetPrefix("ServiceAbbr.");
// The namespace is derived from serviceId only (serviceAbbreviation / serviceFullName are
// no longer read), so the model must carry a serviceId. A deliberately "dirty" serviceId
// (distinct from the serviceAbbreviation above) verifies sanitizeServiceAbbreviation runs
// AND that the transformer reads serviceId, not the abbreviation:
// "AWS My Cool-Service_X" -> strip " -_/", "Amazon", "AWS" -> "MyCoolServiceX".
c2jMetadata.setServiceId("AWS My Cool-Service_X");
c2jMetadata.setUid("service-9089-34-54");
c2jServiceModel.setMetadata(c2jMetadata);
C2jModelToGeneratorModelTransformer c2jModelToGeneratorModelTransformer = new C2jModelToGeneratorModelTransformer(c2jServiceModel, true);
Metadata metadata = c2jModelToGeneratorModelTransformer.convertMetadata();
assertTrue(metadata.isStandalone());
assertTrue(metadata.isApigateway());
assertEquals("MyCoolServiceX", metadata.getNamespace());
assertEquals("service-abbr.execute-api", metadata.getEndpointPrefix());
assertEquals("application-json", metadata.findFirstSupportedProtocol());
}

// A token that cannot appear in a serviceId-derived namespace. If the namespace derivation ever
// read a display-name field, this would leak into the generated identifier.
private static final String DISPLAY_NAME_SENTINEL = "DisplayNameSentinelZZZ";

private static C2jServiceModel modelWithIds(String serviceId, String serviceFullName, String serviceAbbreviation) {
C2jServiceModel model = new C2jServiceModel();
C2jMetadata c2jMetadata = new C2jMetadata();
c2jMetadata.setServiceId(serviceId);
c2jMetadata.setServiceFullName(serviceFullName);
c2jMetadata.setServiceAbbreviation(serviceAbbreviation);
c2jMetadata.setEndpointPrefix("endpoint");
c2jMetadata.setProtocol("json");
c2jMetadata.setApiVersion("2021-01-01");
c2jMetadata.setUid(serviceId + "-2021-01-01");
model.setMetadata(c2jMetadata);
return model;
}

// Guard: the service namespace MUST derive from serviceId only. Swapping the display-name fields
// (serviceFullName / serviceAbbreviation) for a sentinel must not change the derived namespace,
// nor leak the sentinel into it. serviceId NOT in the identifier table.
@Test
public void testNamespaceDerivesFromServiceIdOnly_derivedService() {
Metadata clean = new C2jModelToGeneratorModelTransformer(
modelWithIds("GuardTestService", "AWS Guard Test Service", "GTS"), false).convertMetadata();
Metadata sentineled = new C2jModelToGeneratorModelTransformer(
modelWithIds("GuardTestService", DISPLAY_NAME_SENTINEL + " Full Name", DISPLAY_NAME_SENTINEL), false).convertMetadata();

assertEquals("GuardTestService", clean.getNamespace());
assertEquals(clean.getNamespace(), sentineled.getNamespace());
assertFalse(sentineled.getNamespace().contains(DISPLAY_NAME_SENTINEL));
}

// Same guard for a serviceId that is present in the identifier table (amp -> PrometheusService):
// the preserved name must come from the serviceId-keyed table, never from the display-name fields.
@Test
public void testNamespaceDerivesFromServiceIdOnly_tableService() {
Metadata sentineled = new C2jModelToGeneratorModelTransformer(
modelWithIds("amp", DISPLAY_NAME_SENTINEL + " Full Name", DISPLAY_NAME_SENTINEL), false).convertMetadata();

assertEquals("PrometheusService", sentineled.getNamespace());
assertFalse(sentineled.getNamespace().contains(DISPLAY_NAME_SENTINEL));
}

@Test
public void testStructureShapeConversion() {
C2jServiceModel c2jServiceModel = new C2jServiceModel();
Expand Down
45 changes: 45 additions & 0 deletions tools/code-generation/shared/service-namespace-table.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"AppIntegrations": "AppIntegrationsService",
"CloudControl": "CloudControlApi",
"DataBrew": "GlueDataBrew",
"ECR PUBLIC": "ECRPublic",
"EMR containers": "EMRContainers",
"IVS RealTime": "ivsrealtime",
"Inspector Scan": "inspectorscan",
"IotDeviceAdvisor": "IoTDeviceAdvisor",
"Kinesis Video Signaling": "KinesisVideoSignalingChannels",
"Location": "LocationService",
"Marketplace Agreement": "AgreementService",
"MediaPackageV2": "mediapackagev2",
"MigrationHubStrategy": "MigrationHubStrategyRecommendations",
"OpenSearch": "OpenSearchService",
"RDS Data": "RDSDataService",
"RUM": "CloudWatchRUM",
"Redshift Data": "RedshiftDataAPIService",
"SESv2": "SESV2",
"SageMaker A2I Runtime": "AugmentedAIRuntime",
"Sagemaker Edge": "SagemakerEdgeManager",
"Service Catalog AppRegistry": "AppRegistry",
"Transcribe": "TranscribeService",
"Transcribe Streaming": "TranscribeStreamingService",
"Wisdom": "ConnectWisdomService",
"amp": "PrometheusService",
"b2bi": "B2BI",
"billingconductor": "BillingConductor",
"codeartifact": "CodeArtifact",
"codestar notifications": "CodeStarNotifications",
"evs": "EVS",
"fis": "FIS",
"forecast": "ForecastService",
"forecastquery": "ForecastQueryService",
"grafana": "ManagedGrafana",
"identitystore": "IdentityStore",
"ivs": "IVS",
"m2": "MainframeModernization",
"mq": "MQ",
"rbin": "RecycleBin",
"resiliencehub": "ResilienceHub",
"savingsplans": "SavingsPlans",
"schemas": "Schemas",
"synthetics": "Synthetics"
}
Loading
Loading