diff --git a/lib/aws_codegen.ex b/lib/aws_codegen.ex index a614383..248d6ed 100644 --- a/lib/aws_codegen.ex +++ b/lib/aws_codegen.ex @@ -33,6 +33,7 @@ defmodule AWS.CodeGen do protocol: nil, signature_version: nil, service_id: nil, + shapes: %{}, signing_name: nil, target_prefix: nil end @@ -96,7 +97,7 @@ defmodule AWS.CodeGen do end ) - Enum.each(tasks, fn task -> Task.await(task, 60_000) end) + Enum.each(tasks, fn task -> Task.await(task, 120_000) end) end defp generate_code(spec, language, endpoints_spec, template_base_path, output_path) do diff --git a/lib/aws_codegen/post_service.ex b/lib/aws_codegen/post_service.ex index c32e4bd..f735d5e 100644 --- a/lib/aws_codegen/post_service.ex +++ b/lib/aws_codegen/post_service.ex @@ -1,11 +1,15 @@ defmodule AWS.CodeGen.PostService do alias AWS.CodeGen.Docstring alias AWS.CodeGen.Service + alias AWS.CodeGen.Shapes defmodule Action do defstruct arity: nil, docstring: nil, function_name: nil, + input: nil, + output: nil, + errors: %{}, host_prefix: nil, name: nil end @@ -57,6 +61,7 @@ defmodule AWS.CodeGen.PostService do service = spec.api["shapes"][spec.shape_name] traits = service["traits"] actions = collect_actions(language, spec.api) + shapes = Shapes.collect_shapes(language, spec.api) endpoint_prefix = traits["aws.api#service"]["endpointPrefix"] || traits["aws.api#service"]["arnNamespace"] endpoint_info = endpoints_spec["services"][endpoint_prefix] is_global = not is_nil(endpoint_info) and not Map.get(endpoint_info, "isRegionalized", true) @@ -89,6 +94,7 @@ defmodule AWS.CodeGen.PostService do language: language, module_name: spec.module_name, protocol: protocol |> to_string() |> String.replace("_", "-"), + shapes: shapes, signing_name: signing_name, signature_version: AWS.CodeGen.Util.get_signature_version(service), service_id: AWS.CodeGen.Util.get_service_id(service), @@ -137,10 +143,14 @@ defmodule AWS.CodeGen.PostService do ), function_name: AWS.CodeGen.Name.to_snake_case(operation), host_prefix: operation_spec["traits"]["smithy.api#endpoint"]["hostPrefix"], - name: String.replace(operation, ~r/com\.amazonaws\.[^#]+#/, "") + name: String.replace(operation, ~r/com\.amazonaws\.[^#]+#/, ""), + input: operation_spec["input"], + output: operation_spec["output"], + errors: operation_spec["errors"] } end) |> Enum.sort(fn a, b -> a.function_name < b.function_name end) |> Enum.uniq() end + end diff --git a/lib/aws_codegen/rest_service.ex b/lib/aws_codegen/rest_service.ex index 1b91d69..2b99dcf 100644 --- a/lib/aws_codegen/rest_service.ex +++ b/lib/aws_codegen/rest_service.ex @@ -23,7 +23,10 @@ defmodule AWS.CodeGen.RestService do send_body_as_binary?: false, receive_body_as_binary?: false, host_prefix: nil, - language: nil + language: nil, + input: nil, + output: nil, + errors: [] def method(action) do result = action.method |> String.downcase() |> String.to_atom() @@ -145,7 +148,8 @@ 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), - target_prefix: nil, ##TODO: metadata["targetPrefix"] + target_prefix: nil, ##TODO: metadata["targetPrefix"], + shapes: Shapes.collect_shapes(language, spec.api) } end @@ -157,6 +161,54 @@ defmodule AWS.CodeGen.RestService do function_parameters(action, true) end + def required_function_parameter_types(action) do + function_parameter_types(action.method, action, true) + end + + def required_query_map_types(action) do + function_parameter_types(action.method, action, true) + end + + def function_parameter_types("GET", action, false = _required_only) do + language = action.language + Enum.join( + [join_parameter_types(action.url_parameters, language), + join_parameter_types(action.query_parameters, language), + join_parameter_types(action.request_header_parameters, language), + join_parameter_types(action.request_headers_parameters, language) + ]) + end + def function_parameter_types("GET", action, true = _required_only) do + language = action.language + Enum.join( + [join_parameter_types(action.url_parameters, language), + join_parameter_types(action.required_query_parameters, language), + join_parameter_types(action.required_request_header_parameters, language) + ]) + end + def function_parameter_types(_method, action, _required_only) do + language = action.language + join_parameter_types(action.url_parameters, language) + end + + defp join_parameter_types(parameters, :elixir) do + Enum.join( + Enum.map( + parameters, + fn parameter -> + if not parameter.required do + ", String.t() | nil" + else + ", String.t()" + end + end + ) + ) + end + defp join_parameter_types(parameters, :erlang) do + Enum.join(Enum.map(parameters, fn _parameter -> ", binary() | list()" end)) + end + @doc """ Render function parameters, if any, in a way that can be inserted directly into the code template. It can be asked to only return the required ones. @@ -275,7 +327,10 @@ defmodule AWS.CodeGen.RestService do send_body_as_binary?: Shapes.body_as_binary?(shapes, input_shape), receive_body_as_binary?: Shapes.body_as_binary?(shapes, output_shape), host_prefix: operation_spec["traits"]["smithy.api#endpoint"]["hostPrefix"], - language: language + language: language, + input: operation_spec["input"], + output: operation_spec["output"], + errors: operation_spec["errors"] } end) |> Enum.sort(fn a, b -> a.function_name < b.function_name end) diff --git a/lib/aws_codegen/shapes.ex b/lib/aws_codegen/shapes.ex index 946f9cc..a70269c 100644 --- a/lib/aws_codegen/shapes.ex +++ b/lib/aws_codegen/shapes.ex @@ -1,5 +1,15 @@ defmodule AWS.CodeGen.Shapes do - @moduledoc false + + defmodule Shape do + defstruct name: nil, + type: nil, + members: [], + member: [], + enum: [], + min: nil, + required: [], + is_input: nil + end def get_input_shape(operation_spec) do get_in(operation_spec, ["input", "target"]) @@ -9,6 +19,23 @@ defmodule AWS.CodeGen.Shapes do get_in(operation_spec, ["output", "target"]) end + def collect_shapes(_language, api_spec) do + api_spec["shapes"] + |> Enum.sort(fn {name_a, _}, {name_b, _} -> name_a < name_b end) + |> Map.new(fn {name, shape} -> + {name, + %Shape{ + name: name, + type: shape["type"], + member: shape["member"], + members: shape["members"], + min: shape["min"], + enum: shape["enum"], + is_input: is_input?(shape) + }} + end) + end + def body_as_binary?(shapes, shape) do ## TODO: Should we validate or search for trait `smithy.api#httpPayload` rather than ## trust that the member is always named `Body`? @@ -23,4 +50,8 @@ defmodule AWS.CodeGen.Shapes do end end + def is_input?(shape) do + !Map.has_key?(shape, "traits") or Map.has_key?(shape["traits"], "smithy.api#input") + end + end diff --git a/lib/aws_codegen/types.ex b/lib/aws_codegen/types.ex new file mode 100644 index 0000000..2bb7eae --- /dev/null +++ b/lib/aws_codegen/types.ex @@ -0,0 +1,96 @@ +defmodule AWS.CodeGen.Types do + + alias AWS.CodeGen.Shapes.Shape + + # Unfortunately, gotta patch over auto-defining types that already exist in Elixir + + def shape_to_type(:elixir, "String", _), do: "String.t()" + def shape_to_type(:erlang, "String", _), do: "string()" + def shape_to_type(:elixir, "string", _), do: "String.t()" + def shape_to_type(:erlang, "string", _), do: "string()" + def shape_to_type(:elixir, "Identifier", _), do: "String.t()" + def shape_to_type(:erlang, "Identifier", _), do: "string()" + def shape_to_type(:elixir, "identifier", _), do: "String.t()" + def shape_to_type(:erlang, "identifier", _), do: "string()" + def shape_to_type(:elixir, "XmlString" <> _rest, _), do: "String.t()" + def shape_to_type(:erlang, "XmlString" <> _rest, _), do: "string" + def shape_to_type(:elixir, "NullablePositiveInteger", _), do: "nil | non_neg_integer()" + def shape_to_type(:erlang, "NullablePositiveInteger", _), do: "undefined | non_neg_integer()" + def shape_to_type(_, %Shape{type: type}, _module_name) when type in ["float", "double", "long"], do: "float()" + def shape_to_type(_, %Shape{type: "timestamp"}, _module_name), do: "non_neg_integer()" + def shape_to_type(_, %Shape{type: "map"}, _module_name), do: "map()" + def shape_to_type(_, %Shape{type: "blob"}, _module_name), do: "binary()" + def shape_to_type(:elixir, %Shape{type: "string"}, _module_name), do: "String.t()" + def shape_to_type(:erlang, %Shape{type: "string"}, _module_name), do: "string()" + def shape_to_type(_, %Shape{type: "integer"}, _module_name), do: "integer()" + def shape_to_type(_, %Shape{type: "boolean"}, _module_name), do: "boolean()" + def shape_to_type(_, %Shape{type: "enum"}, _module_name), do: "list(any())" + def shape_to_type(_, %Shape{type: "union"}, _module_name), do: "list()" + def shape_to_type(_, %Shape{type: "document"}, _module_name), do: "any()" + + def shape_to_type(context, shape_name, module_name, all_shapes) do + case shape_name do + "smithy.api#String" -> + "[#{shape_to_type(context.language, %Shape{type: "string"}, module_name)}]" + + "smithy.api#Integer" -> + "[#{shape_to_type(context.language, %Shape{type: "integer"}, module_name)}]" + + "smithy.api#Timestamp" -> + "[#{shape_to_type(context.language, %Shape{type: "timestamp"}, module_name)}]" + + "smithy.api#PrimitiveLong" -> + "[#{shape_to_type(context.language, %Shape{type: "long"}, module_name)}]" + + "smithy.api#Long" -> + "[#{shape_to_type(context.language, %Shape{type: "long"}, module_name)}]" + + "smithy.api#Boolean" -> + "[#{shape_to_type(context.language, %Shape{type: "boolean"}, module_name)}]" + + "smithy.api#PrimitiveBoolean" -> + "[#{shape_to_type(context.language, %Shape{type: "boolean"}, module_name)}]" + + "smithy.api#Double" -> + "[#{shape_to_type(context.language, %Shape{type: "double"}, module_name)}]" + + "smithy.api#Document" -> + "[#{shape_to_type(context.language, %Shape{type: "document"}, module_name)}]" + + "smithy.api#Unit" -> + "[]" + + "smithy.api#Float" -> + "[#{shape_to_type(context.language, %Shape{type: "float"}, module_name)}]" + + "smithy.api#Blob" -> + "[#{shape_to_type(context.language, %Shape{type: "blob"}, module_name)}]" + + _ -> + case all_shapes[shape_name] do + + %Shape{type: "structure"} -> + type = "#{AWS.CodeGen.Name.to_snake_case(String.replace(shape_name, ~r/com\.amazonaws\.[^#]+#/, ""))}" + if AWS.CodeGen.Util.reserved_type(type) do + "#{String.downcase(String.replace(context.module_name, ["aws_", "AWS."], ""))}_#{type}()" + else + "#{type}()" + end + + %Shape{type: "list", member: member} -> + type = "#{shape_to_type(context, member["target"], module_name, all_shapes)}" + if AWS.CodeGen.Util.reserved_type(type) do + "list(#{String.downcase(String.replace(context.module_name, ["aws_", "AWS."], ""))}_#{type}())" + else + "list(#{type}())" + end + + nil -> + raise "Tried to reference an undefined shape for #{shape_name}" + + shape -> + shape_to_type(context.language, shape, module_name) + end + end + end +end diff --git a/lib/aws_codegen/util.ex b/lib/aws_codegen/util.ex index 9c416fa..137dad0 100644 --- a/lib/aws_codegen/util.ex +++ b/lib/aws_codegen/util.ex @@ -30,4 +30,158 @@ defmodule AWS.CodeGen.Util do service["traits"]["aws.api#service"]["sdkId"] end + def input_keys(action, context) do + shapes = context.shapes + input_shape = action.input["target"] + maybe_shape = Enum.filter(shapes, fn {name, _shape} -> input_shape == name end) + case maybe_shape do + [] -> + [] + [{_name, shape}] -> + Enum.reduce(shape.members, + [], + fn {name, %{"traits" => traits}}, acc -> + if Map.has_key?(traits, "smithy.api#required") do + [name <> " Required: true" | acc] + else + [name <> " Required: false" | acc] + end + {name, _shape}, acc -> + [name <> " Required: false" | acc] + end) + |> Enum.reverse() + end + end + + def types(context) do + Enum.reduce(context.shapes, + Map.new(), + fn {_name, shape}, acc -> + if shape.type == "structure" and not is_nil(shape.members) do + type = AWS.CodeGen.Name.to_snake_case(String.replace(shape.name, ~r/com\.amazonaws\.[^#]+#/, "")) + types = Enum.reduce(shape.members, + Map.new(), + fn {name, shape_member}, a -> + target = shape_member["target"] + if Map.has_key?(shape_member, "traits") do + traits = shape_member["traits"] + if Map.has_key?(traits, "smithy.api#httpLabel") do + a + else + shape_member_type = AWS.CodeGen.Types.shape_to_type(context, target, context.module_name, context.shapes) + Map.put(a, is_required(context.language, shape.is_input, shape_member, name), shape_member_type) + end + else + shape_member_type = AWS.CodeGen.Types.shape_to_type(context, target, context.module_name, context.shapes) + Map.put(a, is_required(context.language, shape.is_input, shape_member, name), shape_member_type) + end + end) + if reserved_type(type) do + Map.put(acc, "#{String.downcase(String.replace(context.module_name, "AWS.", ""))}_#{type}", types) + else + Map.put(acc, type, types) + end + else + acc + end + end) + end + + defp is_required(:elixir, is_input, shape, target) do + trimmed_name = String.replace(target, ~r/com\.amazonaws\.[^#]+#/, "") + if is_input do + if Map.has_key?(shape, "traits") do + if Map.has_key?(shape["traits"], "smithy.api#required") do + "required(\"#{trimmed_name}\") => " + else + "optional(\"#{trimmed_name}\") => " + end + else + "optional(\"#{trimmed_name}\") => " + end + else + "\"#{trimmed_name}\" => " + end + end + defp is_required(:erlang, is_input, shape, target) do + trimmed_name = String.replace(target, ~r/com\.amazonaws\.[^#]+#/, "") + if is_input do + if Map.has_key?(shape, "traits") do + if Map.has_key?(shape["traits"], "smithy.api#required") do + "<<\"#{trimmed_name}\">> := " + else + "<<\"#{trimmed_name}\">> => " + end + else + "<<\"#{trimmed_name}\">> => " + end + else + "<<\"#{trimmed_name}\">> => " + end + end + + def function_argument_type(:elixir, action) do + case Map.fetch!(action.input, "target") do + "smithy.api#Unit" -> "%{}" + type -> + "#{AWS.CodeGen.Name.to_snake_case(String.replace(type, ~r/com\.amazonaws\.[^#]+#/, ""))}()" + end + end + def function_argument_type(:erlang, action) do + case Map.fetch!(action.input, "target") do + "smithy.api#Unit" -> "\#{}" + type -> + "#{AWS.CodeGen.Name.to_snake_case(String.replace(type, ~r/com\.amazonaws\.[^#]+#/, ""))}()" + end + end + + def return_type(:elixir, action) do + case Map.fetch!(action.output, "target") do + "smithy.api#Unit" -> + normal = "{:ok, nil, any()}" + errors = + if is_list(action.errors) do + ["{:error, #{action.function_name}_errors()}"] + else + [] + end + Enum.join([normal, "{:error, {:unexpected_response, any()}}" | errors], " | \n") + type -> + normal = "{:ok, #{AWS.CodeGen.Name.to_snake_case(String.replace(type, ~r/com\.amazonaws\.[^#]+#/, ""))}(), any()}" + errors = + if is_list(action.errors) do + ["{:error, #{action.function_name}_errors()}"] + else + [] + end + Enum.join([normal, "{:error, {:unexpected_response, any()}}" | errors], " | \n") + end + end + def return_type(:erlang, action) do + case Map.get(action.output, "target") do + "smithy.api#Unit" -> + normal = "{ok, undefined, tuple()}" + errors = + if is_list(action.errors) do + ["{error, #{action.function_name}_errors(), tuple()}"] + else + [] + end + Enum.join([normal, "{error, any()}" | errors], " |\n ") + type -> + normal = "{ok, #{AWS.CodeGen.Name.to_snake_case(String.replace(type, ~r/com\.amazonaws\.[^#]+#/, ""))}(), tuple()}" + errors = + if is_list(action.errors) do + ["{error, #{action.function_name}_errors(), tuple()}"] + else + [] + end + Enum.join([normal, "{error, any()}" | errors], " |\n ") + end + end + + def reserved_type(type) do + type == "node" || type == "term" || type == "function" || type == "reference" + end + end diff --git a/priv/post.erl.eex b/priv/post.erl.eex index 691b1f3..de51c49 100644 --- a/priv/post.erl.eex +++ b/priv/post.erl.eex @@ -8,14 +8,51 @@ -include_lib("hackney/include/hackney_lib.hrl"). +<%= for {type_name, type_fields} <- AWS.CodeGen.Util.types(context) do %> +%% Example: +%% <%= type_name %>() :: #{ +<%= Enum.map_join(type_fields, ",\n", fn {field_name, field_type} -> + ~s{%% #{field_name}#{field_type}} +end) %> +%% } +-type <%= "#{type_name}()" %> :: #{binary() => any()}. +<% end %> +<%= Enum.map(context.actions, + fn action -> + errors = action.errors + if not is_nil(errors) do + errors_snakecased = errors |> Enum.map(fn error -> AWS.CodeGen.Name.to_snake_case(String.replace(error["target"], ~r/com\.amazonaws\.[^#]+#/, "")) end) + all_types = AWS.CodeGen.Util.types(context) + error_types = Enum.reduce(all_types, + [], + fn {type_name, _type_fields}, acc -> + if Enum.member?(errors_snakecased, type_name) do + ["#{type_name}()" | acc] + else + acc + end + end + ) + "-type #{action.function_name}_errors() ::\n #{Enum.join(error_types, " | \n ")}." + end + end) + |> Enum.reject(&is_nil/1) + |> Enum.join("\n\n") +%> + %%==================================================================== %% API %%==================================================================== <%= for action <- context.actions do %> <%= action.docstring %> +-spec <%= action.function_name %>(map(), <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client, Input) when is_map(Client), is_map(Input) -> <%= action.function_name %>(Client, Input, []). + +-spec <%= action.function_name %>(map(), <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>, proplists:proplist()) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client, Input, Options) when is_map(Client), is_map(Input), is_list(Options) -> request(Client, <<"<%= action.name %>">>, Input, Options). diff --git a/priv/post.ex.eex b/priv/post.ex.eex index fc8bd43..01544a4 100644 --- a/priv/post.ex.eex +++ b/priv/post.ex.eex @@ -11,6 +11,46 @@ defmodule <%= context.module_name %> do alias AWS.Client alias AWS.Request + <%= for {type_name, type_fields} <- AWS.CodeGen.Util.types(context) do %> +@typedoc """ + +## Example: + <%= if map_size(type_fields) == 0 do %> + <%= "#{type_name}() :: %{}" %> + <% else %> + <%= "#{type_name}() :: %{" %> + <%= Enum.map_join(type_fields, ",\n ", fn {field_name, field_type} -> + ~s{ #{field_name}#{field_type}} + end) %> + } + <% end %> +""" +@type <%= if map_size(type_fields) == 0 do "#{type_name}() :: %{}" else "#{type_name}() :: %{String.t => any()}" end %> +<% end %> + +<%= Enum.map(context.actions, + fn action -> + errors = action.errors + if not is_nil(errors) do + errors_snakecased = errors |> Enum.map(fn error -> AWS.CodeGen.Name.to_snake_case(String.replace(error["target"], ~r/com\.amazonaws\.[^#]+#/, "")) end) + all_types = AWS.CodeGen.Util.types(context) + error_types = Enum.reduce(all_types, + [], + fn {type_name, _type_fields}, acc -> + if Enum.member?(errors_snakecased, type_name) do + ["#{type_name}()" | acc] + else + acc + end + end + ) + "@type #{action.function_name}_errors() :: #{Enum.join(error_types, " | ")}" + end + end) + |> Enum.reject(&is_nil/1) + |> Enum.join("\n\n") +%> + def metadata do %{ api_version: <%= inspect(context.api_version) %>, @@ -30,6 +70,7 @@ defmodule <%= context.module_name %> do @doc """ <%= action.docstring %> """<% end %> + @spec <%= action.function_name %>(map(), <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>, list()) :: <%= AWS.CodeGen.Util.return_type(context.language, action)%> def <%= action.function_name %>(%Client{} = client, input, options \\ []) do meta = <%= if action.host_prefix do %> diff --git a/priv/rest.erl.eex b/priv/rest.erl.eex index cfa3a4d..aab847b 100644 --- a/priv/rest.erl.eex +++ b/priv/rest.erl.eex @@ -8,19 +8,60 @@ -include_lib("hackney/include/hackney_lib.hrl"). +<%= for {type_name, type_fields} <- AWS.CodeGen.Util.types(context) do %> +<%= if Map.keys(type_fields) == [] do %>%% Example: +%% <%= type_name %>() :: #{} +-type <%= "#{type_name}()" %> :: #{}.<% else %> +%% Example: +%% <%= type_name %>() :: #{ +<%= Enum.map_join(type_fields, ",\n", fn {field_name, field_type} -> + ~s{%% #{field_name}#{field_type}} +end) %> +%% } +-type <%= "#{type_name}()" %> :: #{binary() => any()}.<% end %> +<% end %> +<%= Enum.map(context.actions, + fn action -> + errors = action.errors + if not is_nil(errors) do + errors_snakecased = errors |> Enum.map(fn error -> AWS.CodeGen.Name.to_snake_case(String.replace(error["target"], ~r/com\.amazonaws\.[^#]+#/, "")) end) + all_types = AWS.CodeGen.Util.types(context) + error_types = Enum.reduce(all_types, + [], + fn {type_name, _type_fields}, acc -> + if Enum.member?(errors_snakecased, type_name) do + ["#{type_name}()" | acc] + else + acc + end + end + ) + "-type #{action.function_name}_errors() ::\n #{Enum.join(error_types, " | \n ")}." + end + end) + |> Enum.reject(&is_nil/1) + |> Enum.join("\n\n") +%> + %%==================================================================== %% API %%==================================================================== <%= for action <- context.actions do %> <%= action.docstring %><%= if action.method == "GET" do %> +-spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.required_function_parameter_types(action) %>) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.required_function_parameters(action) %>) when is_map(Client) -> <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.required_function_parameters(action) %>, #{}, #{}). +-spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.required_function_parameter_types(action) %>, map(), map()) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.required_function_parameters(action) %>, QueryMap, HeadersMap) when is_map(Client), is_map(QueryMap), is_map(HeadersMap) -> <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.required_function_parameters(action) %>, QueryMap, HeadersMap, []). +-spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.required_function_parameter_types(action) %>, map(), map(), proplists:proplist()) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.required_function_parameters(action) %>, QueryMap, HeadersMap, Options0) when is_map(Client), is_map(QueryMap), is_map(HeadersMap), is_list(Options0) -> Path = ["<%= AWS.CodeGen.RestService.Action.url_path(action) %>"],<%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %> @@ -70,8 +111,13 @@ end.<% else %> request(Client, get, Path, Query_, Headers, undefined, Options, SuccessStatusCode<%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %>, Bucket<% end %>).<% end %> <% else %> +-spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.required_function_parameter_types(action) %>, <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.function_parameters(action) %>, Input) -> <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.function_parameters(action) %>, Input, []). + +-spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.required_function_parameter_types(action) %>, <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>, proplists:proplist()) -> + <%= AWS.CodeGen.Util.return_type(context.language, action)%>. <%= action.function_name %>(Client<%= AWS.CodeGen.RestService.function_parameters(action) %>, Input0, Options0) -> Method = <%= AWS.CodeGen.RestService.Action.method(action) %>, Path = ["<%= AWS.CodeGen.RestService.Action.url_path(action) %>"],<%= if AWS.CodeGen.RestService.Context.s3_context?(context) do %> @@ -135,7 +181,7 @@ %% Internal functions %%==================================================================== --spec proplists_take(any(), proplists:proplists(), any()) -> {any(), proplists:proplists()}. +-spec proplists_take(any(), proplists:proplist(), any()) -> {any(), proplists:proplist()}. proplists_take(Key, Proplist, Default) -> Value = proplists:get_value(Key, Proplist, Default), {Value, proplists:delete(Key, Proplist)}. diff --git a/priv/rest.ex.eex b/priv/rest.ex.eex index 003ad39..da450fc 100644 --- a/priv/rest.ex.eex +++ b/priv/rest.ex.eex @@ -11,6 +11,46 @@ defmodule <%= context.module_name %> do alias AWS.Client alias AWS.Request + <%= for {type_name, type_fields} <- AWS.CodeGen.Util.types(context) do %> +@typedoc """ + +## Example: +<%= if map_size(type_fields) == 0 do %> + <%= "#{type_name}() :: %{}" %> +<% else %> + <%= "#{type_name}() :: %{" %> + <%= Enum.map_join(type_fields, ",\n ", fn {field_name, field_type} -> + ~s{ #{field_name}#{field_type}} + end) %> + } +<% end %> +""" +@type <%= if map_size(type_fields) == 0 do "#{type_name}() :: %{}" else "#{type_name}() :: %{String.t => any()}" end %> +<% end %> + +<%= Enum.map(context.actions, + fn action -> + errors = action.errors + if not is_nil(errors) do + errors_snakecased = errors |> Enum.map(fn error -> AWS.CodeGen.Name.to_snake_case(String.replace(error["target"], ~r/com\.amazonaws\.[^#]+#/, "")) end) + all_types = AWS.CodeGen.Util.types(context) + error_types = Enum.reduce(all_types, + [], + fn {type_name, _type_fields}, acc -> + if Enum.member?(errors_snakecased, type_name) do + ["#{type_name}()" | acc] + else + acc + end + end + ) + "@type #{action.function_name}_errors() :: #{Enum.join(error_types, " | ")}" + end + end) + |> Enum.reject(&is_nil/1) + |> Enum.join("\n\n") +%> + def metadata do %{ api_version: <%= inspect(context.api_version) %>, @@ -30,6 +70,7 @@ defmodule <%= context.module_name %> do @doc """ <%= action.docstring %> """<% end %><%= if action.method == "GET" do %> + @spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.function_parameter_types(action.method, action, false)%>, list()) :: <%= AWS.CodeGen.Util.return_type(context.language, action)%> def <%= action.function_name %>(%Client{} = client<%= AWS.CodeGen.RestService.function_parameters(action) %>, options \\ []) do url_path = "<%= AWS.CodeGen.RestService.Action.url_path(action) %>" headers = []<%= for parameter <- action.request_header_parameters do %> @@ -74,7 +115,8 @@ defmodule <%= context.module_name %> do <% end %> Request.request_rest(client, meta, :get, url_path, query_params, headers, nil, options, <%= inspect(action.success_status_code) %>)<% else %> - def <%= action.function_name %>(%Client{} = client<%= AWS.CodeGen.RestService.function_parameters(action) %>, input, options \\ []) do +@spec <%= action.function_name %>(map()<%= AWS.CodeGen.RestService.function_parameter_types(action.method, action, false)%>, <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>, list()) :: <%= AWS.CodeGen.Util.return_type(context.language, action)%> +def <%= action.function_name %>(%Client{} = client<%= AWS.CodeGen.RestService.function_parameters(action) %>, input, options \\ []) do url_path = "<%= AWS.CodeGen.RestService.Action.url_path(action) %>"<%= if length(action.request_header_parameters) > 0 do %> {headers, input} = [<%= for parameter <- action.request_header_parameters do %> diff --git a/test/aws_codegen/rest_service_test.exs b/test/aws_codegen/rest_service_test.exs index ed8bd69..24944f3 100644 --- a/test/aws_codegen/rest_service_test.exs +++ b/test/aws_codegen/rest_service_test.exs @@ -59,10 +59,20 @@ defmodule AWS.CodeGen.RestServiceTest do %RestService.Action{ arity: 3, docstring: " Ingests your application events into CloudTrail Lake.\n\n A required parameter,\n `auditEvents`, accepts the JSON records (also called\n *payload*) of events that you want CloudTrail to ingest. You\n can add up to 100 of these events (or up to 1 MB) per `PutAuditEvents`\n request.", - function_name: "put_audit_events", + errors: [ + %{"target" => "com.amazonaws.cloudtraildata#ChannelInsufficientPermission"}, + %{"target" => "com.amazonaws.cloudtraildata#ChannelNotFound"}, + %{"target" => "com.amazonaws.cloudtraildata#ChannelUnsupportedSchema"}, + %{"target" => "com.amazonaws.cloudtraildata#DuplicatedAuditEventId"}, + %{"target" => "com.amazonaws.cloudtraildata#InvalidChannelARN"}, + %{"target" => "com.amazonaws.cloudtraildata#UnsupportedOperationException"} + ], + function_name: "put_audit_events", language: :elixir, method: "POST", name: "com.amazonaws.cloudtraildata#PutAuditEvents", + input: %{"target" => "com.amazonaws.cloudtraildata#PutAuditEventsRequest"}, + output: %{"target" => "com.amazonaws.cloudtraildata#PutAuditEventsResponse"}, query_parameters: [ %RestService.Parameter{ code_name: "channel_arn", diff --git a/test/aws_codegen_test.exs b/test/aws_codegen_test.exs index de43c64..5504e39 100644 --- a/test/aws_codegen_test.exs +++ b/test/aws_codegen_test.exs @@ -69,105 +69,154 @@ defmodule AWS.CodeGenTest do alias AWS.Client alias AWS.Request - def metadata do - %{ - api_version: "2021-08-11", - content_type: "application/x-amz-json-1.1", - credential_scope: nil, - endpoint_prefix: "cloudtrail-data", - global?: false, - protocol: "rest-json", - service_id: "CloudTrail Data", - signature_version: "v4", - signing_name: "cloudtrail-data", - target_prefix: nil - } - end + @typedoc \"\"\" - @doc \"\"\" - Ingests your application events into CloudTrail Lake. + ## Example: + + audit_event() :: %{ + \"eventData\" => [String.t()], + \"eventDataChecksum\" => [String.t()], + \"id\" => String.t() + } - A required parameter, - `auditEvents`, accepts the JSON records (also called - *payload*) of events that you want CloudTrail to ingest. You - can add up to 100 of these events (or up to 1 MB) per `PutAuditEvents` - request. \"\"\" - def put_audit_events(%Client{} = client, input, options \\\\ []) do - url_path = "/PutAuditEvents" - headers = [] + @type audit_event() :: %{String.t() => any()} - {query_params, input} = - [ - {"channelArn", "channelArn"}, - {"externalId", "externalId"} - ] - |> Request.build_params(input) + @typedoc \"\"\" - meta = metadata() + ## Example: - Request.request_rest( - client, - meta, - :post, - url_path, - query_params, - headers, - input, - options, - 200 - ) - end - end - """) - end + audit_event_result_entry() :: %{ + \"eventID\" => String.t(), + \"id\" => String.t() + } - test "renders POST action with options to send/receive binary", %{specs: specs} do - context = setup_context(:elixir, specs) + \"\"\" + @type audit_event_result_entry() :: %{String.t() => any()} - [action | _] = context.actions - action = %{action | send_body_as_binary?: true, receive_body_as_binary?: true} + @typedoc \"\"\" - result = - %{context | actions: [action]} - |> AWS.CodeGen.render("priv/rest.ex.eex") - |> IO.iodata_to_binary() + ## Example: - assert result == - String.trim_leading(""" - # WARNING: DO NOT EDIT, AUTO-GENERATED CODE! - # See https://github.com/aws-beam/aws-codegen for more details. + channel_insufficient_permission() :: %{ + \"message\" => [String.t()] + } - defmodule AWS.CloudTrailData do - @moduledoc \"\"\" - The CloudTrail Data Service lets you ingest events into CloudTrail from any - source in your - hybrid environments, such as in-house or SaaS applications hosted on-premises or - in the cloud, - virtual machines, or containers. + \"\"\" + @type channel_insufficient_permission() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_not_found() :: %{ + \"message\" => [String.t()] + } - You can store, access, analyze, troubleshoot and take action on - this data without maintaining multiple log aggregators and reporting tools. - After you run - `PutAuditEvents` to ingest your application activity into CloudTrail, you can - use CloudTrail Lake to search, query, and analyze the data that is logged - from your applications. \"\"\" + @type channel_not_found() :: %{String.t() => any()} - alias AWS.Client - alias AWS.Request + @typedoc \"\"\" + + ## Example: + + channel_unsupported_schema() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_unsupported_schema() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + duplicated_audit_event_id() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type duplicated_audit_event_id() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + invalid_channel_arn() :: %{ + \"message\" => [String.t()] + } + + \"\"" + @type invalid_channel_arn() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + put_audit_events_request() :: %{ + optional(\"externalId\") => String.t(), + required(\"auditEvents\") => list(audit_event()()), + required(\"channelArn\") => String.t() + } + + \"\"\" + @type put_audit_events_request() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + put_audit_events_response() :: %{ + required(\"failed\") => list(result_error_entry()()), + required(\"successful\") => list(audit_event_result_entry()()) + } + + \"\"\" + @type put_audit_events_response() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + result_error_entry() :: %{ + \"errorCode\" => String.t(), + \"errorMessage\" => String.t(), + \"id\" => String.t() + } + + \"\"\" + @type result_error_entry() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + unsupported_operation_exception() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type unsupported_operation_exception() :: %{String.t() => any()} + + @type put_audit_events_errors() :: + unsupported_operation_exception() + | invalid_channel_arn() + | duplicated_audit_event_id() + | channel_unsupported_schema() + | channel_not_found() + | channel_insufficient_permission() def metadata do %{ - api_version: "2021-08-11", - content_type: "application/x-amz-json-1.1", + api_version: \"2021-08-11\", + content_type: \"application/x-amz-json-1.1\", credential_scope: nil, - endpoint_prefix: "cloudtrail-data", + endpoint_prefix: \"cloudtrail-data\", global?: false, - protocol: "rest-json", - service_id: "CloudTrail Data", - signature_version: "v4", - signing_name: "cloudtrail-data", + protocol: \"rest-json\", + service_id: \"CloudTrail Data\", + signature_version: \"v4\", + signing_name: \"cloudtrail-data\", target_prefix: nil } end @@ -181,31 +230,21 @@ defmodule AWS.CodeGenTest do can add up to 100 of these events (or up to 1 MB) per `PutAuditEvents` request. \"\"\" + @spec put_audit_events(map(), put_audit_events_request(), list()) :: + {:ok, put_audit_events_response(), any()} + | {:error, {:unexpected_response, any()}} + | {:error, put_audit_events_errors()} def put_audit_events(%Client{} = client, input, options \\\\ []) do - url_path = "/PutAuditEvents" + url_path = \"/PutAuditEvents\" headers = [] {query_params, input} = [ - {"channelArn", "channelArn"}, - {"externalId", "externalId"} + {\"channelArn\", \"channelArn\"}, + {\"externalId\", \"externalId\"} ] |> Request.build_params(input) - options = - Keyword.put( - options, - :send_body_as_binary?, - true - ) - - options = - Keyword.put( - options, - :receive_body_as_binary?, - true - ) - meta = metadata() Request.request_rest( @@ -265,17 +304,154 @@ defmodule AWS.CodeGenTest do alias AWS.Client alias AWS.Request + @typedoc \"\"\" + + ## Example: + + audit_event() :: %{ + \"eventData\" => [String.t()], + \"eventDataChecksum\" => [String.t()], + \"id\" => String.t() + } + + \"\"\" + @type audit_event() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + audit_event_result_entry() :: %{ + \"eventID\" => String.t(), + \"id\" => String.t() + } + + \"\"\" + @type audit_event_result_entry() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_insufficient_permission() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_insufficient_permission() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_not_found() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_not_found() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_unsupported_schema() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_unsupported_schema() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + duplicated_audit_event_id() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type duplicated_audit_event_id() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + invalid_channel_arn() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type invalid_channel_arn() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + put_audit_events_request() :: %{ + optional(\"externalId\") => String.t(), + required(\"auditEvents\") => list(audit_event()()), + required(\"channelArn\") => String.t() + } + + \"\"\" + @type put_audit_events_request() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + put_audit_events_response() :: %{ + required(\"failed\") => list(result_error_entry()()), + required(\"successful\") => list(audit_event_result_entry()()) + } + + \"\"\" + @type put_audit_events_response() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + result_error_entry() :: %{ + \"errorCode\" => String.t(), + \"errorMessage\" => String.t(), + \"id\" => String.t() + } + + \"\"\" + @type result_error_entry() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + unsupported_operation_exception() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type unsupported_operation_exception() :: %{String.t() => any()} + + @type put_audit_events_errors() :: + unsupported_operation_exception() + | invalid_channel_arn() + | duplicated_audit_event_id() + | channel_unsupported_schema() + | channel_not_found() + | channel_insufficient_permission() + def metadata do %{ - api_version: "2021-08-11", - content_type: "application/x-amz-json-1.1", + api_version: \"2021-08-11\", + content_type: \"application/x-amz-json-1.1\", credential_scope: nil, - endpoint_prefix: "cloudtrail-data", + endpoint_prefix: \"cloudtrail-data\", global?: false, - protocol: "rest-json", - service_id: "CloudTrail Data", - signature_version: "v4", - signing_name: "cloudtrail-data", + protocol: \"rest-json\", + service_id: \"CloudTrail Data\", + signature_version: \"v4\", + signing_name: \"cloudtrail-data\", target_prefix: nil } end @@ -289,8 +465,12 @@ defmodule AWS.CodeGenTest do can add up to 100 of these events (or up to 1 MB) per `PutAuditEvents` request. \"\"\" + @spec put_audit_events(map(), String.t(), String.t() | nil, list()) :: + {:ok, put_audit_events_response(), any()} + | {:error, {:unexpected_response, any()}} + | {:error, put_audit_events_errors()} def put_audit_events(%Client{} = client, channel_arn, external_id \\\\ nil, options \\\\ []) do - url_path = "/PutAuditEvents" + url_path = \"/PutAuditEvents\" headers = [] query_params = [] @@ -366,17 +546,154 @@ defmodule AWS.CodeGenTest do alias AWS.Client alias AWS.Request + @typedoc \"\"\" + + ## Example: + + audit_event() :: %{ + \"eventData\" => [String.t()], + \"eventDataChecksum\" => [String.t()], + \"id\" => String.t() + } + + \"\"\" + @type audit_event() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + audit_event_result_entry() :: %{ + \"eventID\" => String.t(), + \"id\" => String.t() + } + + \"\"\" + @type audit_event_result_entry() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_insufficient_permission() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_insufficient_permission() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_not_found() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_not_found() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + channel_unsupported_schema() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type channel_unsupported_schema() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + duplicated_audit_event_id() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type duplicated_audit_event_id() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + invalid_channel_arn() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type invalid_channel_arn() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + put_audit_events_request() :: %{ + optional(\"externalId\") => String.t(), + required(\"auditEvents\") => list(audit_event()()), + required(\"channelArn\") => String.t() + } + + \"\"\" + @type put_audit_events_request() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + put_audit_events_response() :: %{ + required(\"failed\") => list(result_error_entry()()), + required(\"successful\") => list(audit_event_result_entry()()) + } + + \"\"\" + @type put_audit_events_response() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + result_error_entry() :: %{ + \"errorCode\" => String.t(), + \"errorMessage\" => String.t(), + \"id\" => String.t() + } + + \"\"\" + @type result_error_entry() :: %{String.t() => any()} + + @typedoc \"\"\" + + ## Example: + + unsupported_operation_exception() :: %{ + \"message\" => [String.t()] + } + + \"\"\" + @type unsupported_operation_exception() :: %{String.t() => any()} + + @type put_audit_events_errors() :: + unsupported_operation_exception() + | invalid_channel_arn() + | duplicated_audit_event_id() + | channel_unsupported_schema() + | channel_not_found() + | channel_insufficient_permission() + def metadata do %{ - api_version: "2021-08-11", - content_type: "application/x-amz-json-1.1", + api_version: \"2021-08-11\", + content_type: \"application/x-amz-json-1.1\", credential_scope: nil, - endpoint_prefix: "cloudtrail-data", + endpoint_prefix: \"cloudtrail-data\", global?: false, - protocol: "rest-json", - service_id: "CloudTrail Data", - signature_version: "v4", - signing_name: "cloudtrail-data", + protocol: \"rest-json\", + service_id: \"CloudTrail Data\", + signature_version: \"v4\", + signing_name: \"cloudtrail-data\", target_prefix: nil } end @@ -390,18 +707,22 @@ defmodule AWS.CodeGenTest do can add up to 100 of these events (or up to 1 MB) per `PutAuditEvents` request. \"\"\" + @spec put_audit_events(map(), put_audit_events_request(), list()) :: + {:ok, put_audit_events_response(), any()} + | {:error, {:unexpected_response, any()}} + | {:error, put_audit_events_errors()} def put_audit_events(%Client{} = client, input, options \\\\ []) do - url_path = "/PutAuditEvents" + url_path = \"/PutAuditEvents\" headers = [] {query_params, input} = [ - {"channelArn", "channelArn"}, - {"externalId", "externalId"} + {\"channelArn\", \"channelArn\"}, + {\"externalId\", \"externalId\"} ] |> Request.build_params(input) - meta = metadata() |> Map.put_new(:host_prefix, "my-host-prefix.") + meta = metadata() |> Map.put_new(:host_prefix, \"my-host-prefix.\") Request.request_rest( client,