From 39090dfc22cdbc8e5900e0ed16fb95ddebfe941b Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Fri, 22 May 2026 15:17:43 +0100 Subject: [PATCH 1/4] Honor AWS_ENDPOINT_URL_ env vars in generated Erlang clients Depends on aws_beam_core:apply_endpoint_url_override --- lib/aws_codegen.ex | 5 ++++ lib/aws_codegen/post_service.ex | 1 + lib/aws_codegen/rest_service.ex | 1 + lib/aws_codegen/util.ex | 30 ++++++++++++++++++++++++ priv/post.erl.eex | 6 +++-- priv/rest.erl.eex | 11 +++++---- test/aws_codegen/rest_service_test.exs | 1 + test/aws_codegen/util_test.exs | 32 ++++++++++++++++++++++++++ 8 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 test/aws_codegen/util_test.exs diff --git a/lib/aws_codegen.ex b/lib/aws_codegen.ex index 8fc0483..91b66bc 100644 --- a/lib/aws_codegen.ex +++ b/lib/aws_codegen.ex @@ -26,6 +26,11 @@ defmodule AWS.CodeGen do decode: nil, encode: nil, endpoint_prefix: nil, + # Name of the AWS-canonical service-specific endpoint env var + # (e.g. "AWS_ENDPOINT_URL_DYNAMODB"). Derived from `service_id` + # per the AWS CLI/SDK convention: spaces -> "_", uppercased. + # See https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-endpoints.html + endpoint_url_env_var: nil, is_global: false, hostname: nil, json_version: nil, diff --git a/lib/aws_codegen/post_service.ex b/lib/aws_codegen/post_service.ex index 9a4fb4f..2a9257e 100644 --- a/lib/aws_codegen/post_service.ex +++ b/lib/aws_codegen/post_service.ex @@ -111,6 +111,7 @@ defmodule AWS.CodeGen.PostService do signing_name: signing_name, signature_version: AWS.CodeGen.Util.get_signature_version(service), service_id: AWS.CodeGen.Util.get_service_id(service), + endpoint_url_env_var: AWS.CodeGen.Util.endpoint_url_env_var(service), target_prefix: target_prefix(spec.api) } end diff --git a/lib/aws_codegen/rest_service.ex b/lib/aws_codegen/rest_service.ex index c9f3795..cf2d95b 100644 --- a/lib/aws_codegen/rest_service.ex +++ b/lib/aws_codegen/rest_service.ex @@ -157,6 +157,7 @@ defmodule AWS.CodeGen.RestService do signing_name: signing_name, signature_version: AWS.CodeGen.Util.get_signature_version(service), service_id: AWS.CodeGen.Util.get_service_id(service), + endpoint_url_env_var: AWS.CodeGen.Util.endpoint_url_env_var(service), ## TODO: metadata["targetPrefix"], target_prefix: nil, shapes: Shapes.collect_shapes(language, spec.api) diff --git a/lib/aws_codegen/util.ex b/lib/aws_codegen/util.ex index d35ca6b..cbe8d25 100644 --- a/lib/aws_codegen/util.ex +++ b/lib/aws_codegen/util.ex @@ -35,6 +35,36 @@ defmodule AWS.CodeGen.Util do service["traits"]["aws.api#service"]["sdkId"] end + @doc """ + Derive the AWS-canonical service-specific endpoint env var name from the + service's `sdkId` (a.k.a. `service_id`). + + Per https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-endpoints.html + the env var suffix is the `sdkId` with spaces replaced by `_` and the + whole string uppercased. The generic prefix is `AWS_ENDPOINT_URL_`. + + Examples: + + iex> AWS.CodeGen.Util.endpoint_url_env_var(%{"traits" => %{"aws.api#service" => %{"sdkId" => "DynamoDB"}}}) + "AWS_ENDPOINT_URL_DYNAMODB" + + iex> AWS.CodeGen.Util.endpoint_url_env_var(%{"traits" => %{"aws.api#service" => %{"sdkId" => "Elastic Beanstalk"}}}) + "AWS_ENDPOINT_URL_ELASTIC_BEANSTALK" + + iex> AWS.CodeGen.Util.endpoint_url_env_var(%{"traits" => %{"aws.api#service" => %{"sdkId" => "CloudTrail Data"}}}) + "AWS_ENDPOINT_URL_CLOUDTRAIL_DATA" + """ + def endpoint_url_env_var(service) do + case get_service_id(service) do + nil -> + nil + + service_id -> + suffix = service_id |> String.replace(" ", "_") |> String.upcase() + "AWS_ENDPOINT_URL_" <> suffix + end + end + def input_keys(action, context) do shapes = context.shapes input_shape = action.input["target"] diff --git a/priv/post.erl.eex b/priv/post.erl.eex index 6a42686..f5a8e5f 100644 --- a/priv/post.erl.eex +++ b/priv/post.erl.eex @@ -75,8 +75,10 @@ request(Client, Action, Input, Options) -> <% else %>do_request(Client, Action, Input0, Options) -> <% end %> Client1 = Client#{service => <<"<%= context.signing_name %>">><%= if context.is_global do %>, region => <<"<%= context.credential_scope %>">><% end %>}, - Host = build_host(<<"<%= context.endpoint_prefix %>">>, Client1), - URL = build_url(Host, Client1), + DefaultHost = build_host(<<"<%= context.endpoint_prefix %>">>, Client1), + {URL, Host} = aws_util:apply_endpoint_url_override( + build_url(DefaultHost, Client1), DefaultHost, <<"/">>, + <<"<%= context.endpoint_url_env_var %>">>), Headers = [ {<<"Host">>, Host}, {<<"Content-Type">>, <<"<%= context.content_type %>">>}<%= if context.protocol == "json" do %>, diff --git a/priv/rest.erl.eex b/priv/rest.erl.eex index 251f320..29840ec 100644 --- a/priv/rest.erl.eex +++ b/priv/rest.erl.eex @@ -209,9 +209,13 @@ do_request(Client, Method, Path, Query, Headers0, Input, Options, SuccessStatusC Client1 = Client#{service => <<"<%= context.signing_name %>">><%= if context.is_global do %>, region => <<"<%= context.credential_scope %>">><% end %>}, <%= if context.endpoint_prefix == "s3-control" do %>AccountId = proplists:get_value(<<"x-amz-account-id">>, Headers0), - Host = build_host(AccountId, <<"<%= context.endpoint_prefix %>">>, Client1),<% else %><%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>Host = build_host(<<"<%= context.endpoint_prefix %>">>, Client1, Bucket),<%else %>Host = build_host(<<"<%= context.endpoint_prefix %>">>, Client1),<% end %><% end %> - URL0 = build_url(Host, Path, Client1<%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>, Bucket<% end %>), - URL = aws_request:add_query(URL0, Query), + DefaultHost = build_host(AccountId, <<"<%= context.endpoint_prefix %>">>, Client1),<% else %><%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>DefaultHost = build_host(<<"<%= context.endpoint_prefix %>">>, Client1, Bucket),<%else %>DefaultHost = build_host(<<"<%= context.endpoint_prefix %>">>, Client1),<% end %><% end %> + URL0 = build_url(DefaultHost, Path, Client1<%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>, Bucket<% end %>), + PathBin = erlang:iolist_to_binary(Path), + {URL1, Host} = aws_util:apply_endpoint_url_override( + URL0, DefaultHost, PathBin, + <<"<%= context.endpoint_url_env_var %>">>), + URL = aws_request:add_query(URL1, Query), AdditionalHeaders1 = [ {<<"Host">>, Host} , {<<"Content-Type">>, <<"<%= context.content_type %>">>} ], @@ -332,7 +336,6 @@ build_host(EndpointPrefix, #{endpoint := Endpoint}) -> aws_util:binary_join([EndpointPrefix, Endpoint], <<".">>).<% else %> build_host(EndpointPrefix, #{region := Region, endpoint := Endpoint}) -> aws_util:binary_join([EndpointPrefix, Region, Endpoint], <<".">>).<% end %><% end %><% end %><% end %> - <%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>build_url(Host0, Path0, Client, Bucket) -> Proto = aws_client:proto(Client), %% Mocks are notoriously bad with host-style requests, just skip it and use path-style for anything local diff --git a/test/aws_codegen/rest_service_test.exs b/test/aws_codegen/rest_service_test.exs index cd45c61..e38a160 100644 --- a/test/aws_codegen/rest_service_test.exs +++ b/test/aws_codegen/rest_service_test.exs @@ -51,6 +51,7 @@ defmodule AWS.CodeGen.RestServiceTest do module_name: "AWS.CloudTrailData", protocol: "rest-json", service_id: "CloudTrail Data", + endpoint_url_env_var: "AWS_ENDPOINT_URL_CLOUDTRAIL_DATA", signature_version: "v4", signing_name: "cloudtrail-data", target_prefix: nil diff --git a/test/aws_codegen/util_test.exs b/test/aws_codegen/util_test.exs new file mode 100644 index 0000000..1b1bcc3 --- /dev/null +++ b/test/aws_codegen/util_test.exs @@ -0,0 +1,32 @@ +defmodule AWS.CodeGen.UtilTest do + use ExUnit.Case, async: true + doctest AWS.CodeGen.Util + + alias AWS.CodeGen.Util + + describe "endpoint_url_env_var/1" do + test "derives the env var from a single-word sdkId" do + service = build_service("DynamoDB") + assert Util.endpoint_url_env_var(service) == "AWS_ENDPOINT_URL_DYNAMODB" + end + + test "replaces interior spaces with underscores and uppercases the result" do + service = build_service("Elastic Beanstalk") + assert Util.endpoint_url_env_var(service) == "AWS_ENDPOINT_URL_ELASTIC_BEANSTALK" + end + + test "handles multi-word sdkIds" do + service = build_service("CloudTrail Data") + assert Util.endpoint_url_env_var(service) == "AWS_ENDPOINT_URL_CLOUDTRAIL_DATA" + end + + test "returns nil when the sdkId is missing" do + assert Util.endpoint_url_env_var(%{"traits" => %{}}) == nil + assert Util.endpoint_url_env_var(%{}) == nil + end + end + + defp build_service(sdk_id) do + %{"traits" => %{"aws.api#service" => %{"sdkId" => sdk_id}}} + end +end From 4a44eaffa485a54f11bba8427782cb543414d236 Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Wed, 27 May 2026 10:11:46 +0100 Subject: [PATCH 2/4] Update lib/aws_codegen/post_service.ex Co-authored-by: Onno Vos --- lib/aws_codegen/post_service.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws_codegen/post_service.ex b/lib/aws_codegen/post_service.ex index 2a9257e..55edf02 100644 --- a/lib/aws_codegen/post_service.ex +++ b/lib/aws_codegen/post_service.ex @@ -111,7 +111,7 @@ defmodule AWS.CodeGen.PostService do signing_name: signing_name, signature_version: AWS.CodeGen.Util.get_signature_version(service), service_id: AWS.CodeGen.Util.get_service_id(service), - endpoint_url_env_var: AWS.CodeGen.Util.endpoint_url_env_var(service), + endpoint_url_env_var: "AWS_ENDPOINT_URL_" <> spec.module_name |> String.upcase(), target_prefix: target_prefix(spec.api) } end From 9fc795a8148a910326a1e045f396394f65326b14 Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Wed, 27 May 2026 10:12:21 +0100 Subject: [PATCH 3/4] Update lib/aws_codegen/rest_service.ex Co-authored-by: Onno Vos --- lib/aws_codegen/rest_service.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws_codegen/rest_service.ex b/lib/aws_codegen/rest_service.ex index cf2d95b..500a237 100644 --- a/lib/aws_codegen/rest_service.ex +++ b/lib/aws_codegen/rest_service.ex @@ -157,7 +157,7 @@ defmodule AWS.CodeGen.RestService do signing_name: signing_name, signature_version: AWS.CodeGen.Util.get_signature_version(service), service_id: AWS.CodeGen.Util.get_service_id(service), - endpoint_url_env_var: AWS.CodeGen.Util.endpoint_url_env_var(service), + endpoint_url_env_var: "AWS_ENDPOINT_URL_" <> spec.module_name |> String.upcase(), ## TODO: metadata["targetPrefix"], target_prefix: nil, shapes: Shapes.collect_shapes(language, spec.api) From 657b62fcb25d252eb069449e96a72dca4ff6763c Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Wed, 27 May 2026 10:15:30 +0100 Subject: [PATCH 4/4] remove extra util + tests and fix indent --- lib/aws_codegen/util.ex | 30 ------------------------------ priv/post.erl.eex | 4 +--- priv/rest.erl.eex | 4 +--- test/aws_codegen/util_test.exs | 32 -------------------------------- 4 files changed, 2 insertions(+), 68 deletions(-) delete mode 100644 test/aws_codegen/util_test.exs diff --git a/lib/aws_codegen/util.ex b/lib/aws_codegen/util.ex index cbe8d25..d35ca6b 100644 --- a/lib/aws_codegen/util.ex +++ b/lib/aws_codegen/util.ex @@ -35,36 +35,6 @@ defmodule AWS.CodeGen.Util do service["traits"]["aws.api#service"]["sdkId"] end - @doc """ - Derive the AWS-canonical service-specific endpoint env var name from the - service's `sdkId` (a.k.a. `service_id`). - - Per https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-endpoints.html - the env var suffix is the `sdkId` with spaces replaced by `_` and the - whole string uppercased. The generic prefix is `AWS_ENDPOINT_URL_`. - - Examples: - - iex> AWS.CodeGen.Util.endpoint_url_env_var(%{"traits" => %{"aws.api#service" => %{"sdkId" => "DynamoDB"}}}) - "AWS_ENDPOINT_URL_DYNAMODB" - - iex> AWS.CodeGen.Util.endpoint_url_env_var(%{"traits" => %{"aws.api#service" => %{"sdkId" => "Elastic Beanstalk"}}}) - "AWS_ENDPOINT_URL_ELASTIC_BEANSTALK" - - iex> AWS.CodeGen.Util.endpoint_url_env_var(%{"traits" => %{"aws.api#service" => %{"sdkId" => "CloudTrail Data"}}}) - "AWS_ENDPOINT_URL_CLOUDTRAIL_DATA" - """ - def endpoint_url_env_var(service) do - case get_service_id(service) do - nil -> - nil - - service_id -> - suffix = service_id |> String.replace(" ", "_") |> String.upcase() - "AWS_ENDPOINT_URL_" <> suffix - end - end - def input_keys(action, context) do shapes = context.shapes input_shape = action.input["target"] diff --git a/priv/post.erl.eex b/priv/post.erl.eex index f5a8e5f..544ce52 100644 --- a/priv/post.erl.eex +++ b/priv/post.erl.eex @@ -76,9 +76,7 @@ request(Client, Action, Input, Options) -> <% end %> Client1 = Client#{service => <<"<%= context.signing_name %>">><%= if context.is_global do %>, region => <<"<%= context.credential_scope %>">><% end %>}, DefaultHost = build_host(<<"<%= context.endpoint_prefix %>">>, Client1), - {URL, Host} = aws_util:apply_endpoint_url_override( - build_url(DefaultHost, Client1), DefaultHost, <<"/">>, - <<"<%= context.endpoint_url_env_var %>">>), + {URL, Host} = aws_util:apply_endpoint_url_override(build_url(DefaultHost, Client1), DefaultHost, <<"/">>, <<"<%= context.endpoint_url_env_var %>">>), Headers = [ {<<"Host">>, Host}, {<<"Content-Type">>, <<"<%= context.content_type %>">>}<%= if context.protocol == "json" do %>, diff --git a/priv/rest.erl.eex b/priv/rest.erl.eex index 29840ec..1b4664a 100644 --- a/priv/rest.erl.eex +++ b/priv/rest.erl.eex @@ -212,9 +212,7 @@ do_request(Client, Method, Path, Query, Headers0, Input, Options, SuccessStatusC DefaultHost = build_host(AccountId, <<"<%= context.endpoint_prefix %>">>, Client1),<% else %><%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>DefaultHost = build_host(<<"<%= context.endpoint_prefix %>">>, Client1, Bucket),<%else %>DefaultHost = build_host(<<"<%= context.endpoint_prefix %>">>, Client1),<% end %><% end %> URL0 = build_url(DefaultHost, Path, Client1<%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>, Bucket<% end %>), PathBin = erlang:iolist_to_binary(Path), - {URL1, Host} = aws_util:apply_endpoint_url_override( - URL0, DefaultHost, PathBin, - <<"<%= context.endpoint_url_env_var %>">>), + {URL1, Host} = aws_util:apply_endpoint_url_override(URL0, DefaultHost, PathBin, <<"<%= context.endpoint_url_env_var %>">>), URL = aws_request:add_query(URL1, Query), AdditionalHeaders1 = [ {<<"Host">>, Host} , {<<"Content-Type">>, <<"<%= context.content_type %>">>} diff --git a/test/aws_codegen/util_test.exs b/test/aws_codegen/util_test.exs deleted file mode 100644 index 1b1bcc3..0000000 --- a/test/aws_codegen/util_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -defmodule AWS.CodeGen.UtilTest do - use ExUnit.Case, async: true - doctest AWS.CodeGen.Util - - alias AWS.CodeGen.Util - - describe "endpoint_url_env_var/1" do - test "derives the env var from a single-word sdkId" do - service = build_service("DynamoDB") - assert Util.endpoint_url_env_var(service) == "AWS_ENDPOINT_URL_DYNAMODB" - end - - test "replaces interior spaces with underscores and uppercases the result" do - service = build_service("Elastic Beanstalk") - assert Util.endpoint_url_env_var(service) == "AWS_ENDPOINT_URL_ELASTIC_BEANSTALK" - end - - test "handles multi-word sdkIds" do - service = build_service("CloudTrail Data") - assert Util.endpoint_url_env_var(service) == "AWS_ENDPOINT_URL_CLOUDTRAIL_DATA" - end - - test "returns nil when the sdkId is missing" do - assert Util.endpoint_url_env_var(%{"traits" => %{}}) == nil - assert Util.endpoint_url_env_var(%{}) == nil - end - end - - defp build_service(sdk_id) do - %{"traits" => %{"aws.api#service" => %{"sdkId" => sdk_id}}} - end -end