diff --git a/README.md b/README.md index f253526..6ed84dd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Code is generated by running the `generate.exs` script. It requires Elixir 1.8.4 ### Elixir ```bash -export SPEC_PATH=../../aws/aws-sdk-go/models/apis +export SPEC_PATH=../aws-sdk-go-v2/codegen/sdk-codegen/aws-models export TEMPLATE_PATH=priv export ELIXIR_OUTPUT_PATH=../aws-elixir/lib/aws/generated mix run generate.exs elixir $SPEC_PATH $TEMPLATE_PATH $ELIXIR_OUTPUT_PATH @@ -21,7 +21,7 @@ mix run generate.exs elixir $SPEC_PATH $TEMPLATE_PATH $ELIXIR_OUTPUT_PATH ### Erlang ```bash -export SPEC_PATH=../../aws/aws-sdk-go/models/apis +export SPEC_PATH=../aws-sdk-go-v2/codegen/sdk-codegen/aws-models export TEMPLATE_PATH=priv export ERLANG_OUTPUT_PATH=../aws-erlang/src mix run generate.exs erlang $SPEC_PATH $TEMPLATE_PATH $ERLANG_OUTPUT_PATH @@ -75,5 +75,5 @@ Alternatively you can install XCode's Command Line Developer Tools package: $ xcode-select --install -[aws-sdk-go]: https://github.com/aws/aws-sdk-go +[aws-sdk-go]: https://github.com/aws/aws-sdk-go-v2 [brew]: https://brew.sh/ diff --git a/lib/aws_codegen.ex b/lib/aws_codegen.ex index 8fc0483..a47fd69 100644 --- a/lib/aws_codegen.ex +++ b/lib/aws_codegen.ex @@ -1,4 +1,5 @@ defmodule AWS.CodeGen do + alias AWS.CodeGen.RestService.Parameter alias AWS.CodeGen.Spec @moduledoc """ @@ -132,6 +133,24 @@ defmodule AWS.CodeGen do format_string!(context.language, rendered) end + @param_quoted_elixir EEx.compile_string( + ~s|* `:<%= parameter.code_name %>` (`t:<%= parameter.type %>`<%= if parameter.required do %> required<% end %>) <%= parameter.docs %>| + ) + def render_parameter(:elixir, %Parameter{} = parameter) do + Code.eval_quoted(@param_quoted_elixir, parameter: parameter) + |> then(&elem(&1, 0)) + |> Excribe.format(width: 80, hanging: 4, pretty: true) + end + + @body_param_quoted EEx.compile_string( + ~s|* `"<%= parameter.location_name %>" => t:<%= parameter.type %>`<%= if parameter.required do %> (required)<% end %> <%= parameter.docs %>| + ) + def render_body_parameter(%Parameter{} = parameter) do + Code.eval_quoted(@body_param_quoted, parameter: parameter) + |> then(&elem(&1, 0)) + |> Excribe.format(width: 80, hanging: 4, pretty: true) + end + defp format_string!(:elixir, rendered) do [Code.format_string!(rendered), ?\n] end diff --git a/lib/aws_codegen/docstring.ex b/lib/aws_codegen/docstring.ex index 2a5e21f..c23f880 100644 --- a/lib/aws_codegen/docstring.ex +++ b/lib/aws_codegen/docstring.ex @@ -10,18 +10,33 @@ defmodule AWS.CodeGen.Docstring do heredoc in generated Elixir or Erlang code. """ def format(:elixir, text) do - text - |> html_to_markdown() - |> split_first_sentence_in_one_line() - |> split_paragraphs() - |> Enum.map(&justify_line(&1, @max_elixir_line_length)) - |> Enum.join("\n") - |> fix_broken_markdown_links() - |> fix_elixir_lookalike_format_strings() - |> fix_html_spaces() - |> fix_long_break_lines() - |> transform_subtitles() - |> String.trim_trailing() + docstring = + text + |> html_to_markdown() + |> fix_broken_markdown_links() + |> fix_elixir_lookalike_format_strings() + |> fix_html_spaces() + |> fix_long_break_lines() + |> transform_subtitles() + |> String.trim_trailing() + + # Split off the beginning of the docs. + [docstring_header, _docstring_rest] = + case String.split(docstring, "\n", parts: 3, trim: true) do + [a, b, rest] -> + [a <> "\n" <> b, rest] + + [a, b] -> + [a, b] + + [a] -> + [a, ""] + + [] -> + ["", ""] + end + + docstring_header |> Excribe.format(width: 80, hanging: 2) end def format(:erlang, nil), do: "" @@ -149,7 +164,7 @@ defmodule AWS.CodeGen.Docstring do defp update_nodes({tag, _, children}) when tag in ~w(i em), do: "*#{Floki.text(children)}*" defp update_nodes({tag, _, children}) when tag in ~w(p fullname note important), - do: Floki.text(children) <> @two_break_lines + do: (Floki.text(children) |> String.replace("\n", " ")) <> @two_break_lines defp update_nodes({"a", attrs, children}) do case Enum.find(attrs, fn {attr, _} -> attr == "href" end) do @@ -367,4 +382,22 @@ defmodule AWS.CodeGen.Docstring do List.flatten(lines ++ [current]) end + + def docs_url(shapes, operation) do + service_name = + shapes + |> Map.keys() + |> List.first() + |> then(fn name -> + name + |> String.split("#") + |> hd() + |> String.split(".") + |> List.last() + end) + + op_name = operation |> String.split("#") |> List.last() + + "https://docs.aws.amazon.com/search/doc-search.html?searchPath=documentation&searchQuery=#{service_name}%20#{op_name}&this_doc_guide=API%2520Reference" + end end diff --git a/lib/aws_codegen/elixir_helpers.ex b/lib/aws_codegen/elixir_helpers.ex new file mode 100644 index 0000000..c62c1b7 --- /dev/null +++ b/lib/aws_codegen/elixir_helpers.ex @@ -0,0 +1,285 @@ +defmodule AWS.CodeGen.ElixirHelpers do + alias AWS.CodeGen.PostService + alias AWS.CodeGen.RestService + require EEx + + @validate_quoted EEx.compile_string(~s{ + # Validate optional parameters + optional_params = [<%= Enum.map(action.optional_query_parameters ++ action.optional_request_header_parameters ++ action.request_headers_parameters, &(&1.code_name <> ": nil")) |> Enum.join(", ") %>] + options = Keyword.validate!(options, [enable_retries?: false, retry_num: 0, retry_opts: []] ++ optional_params) + }) + def define_and_validate_optionals(action) do + {res, _} = Code.eval_quoted(@validate_quoted, action: action) + res + end + + @drop_optionals_quoted EEx.compile_string(~s{ + # Drop optionals that have been moved to query/header-params + options = options + |> Keyword.drop([<%= action.optional_query_parameters ++ action.optional_request_header_parameters |> Enum.map(fn act -> ":" <> act.code_name end) |> Enum.join(", ") %>]) + }) + def drop_optionals(action) do + if Enum.empty?(action.optional_query_parameters) and + Enum.empty?(action.optional_request_header_parameters) do + # Don't drop anything, if there are no optional params + nil + else + # Drop the optional params + {res, _} = Code.eval_quoted(@drop_optionals_quoted, action: action) + res + end + end + + def render_type_fields(_type_name, type_fields, indent \\ 4) do + indent_str = String.duplicate(" ", indent) + + Enum.map_join(type_fields, ",\n" <> indent_str, fn {field_name, field_type} -> + field_name <> field_type + end) + end + + @docstring_rest_quoted EEx.compile_string(~s{@doc """ + <%= if String.trim(action.docstring) != "" do + %><%= action.docstring %><% end %> + + [API Reference](<%= action.docs_url %>) + + ## Parameters:<%= for parameter <- action.url_parameters do %> + <%= AWS.CodeGen.render_parameter(:elixir, parameter) %><% end + %><%= for parameter <- action.required_query_parameters do %> + <%= AWS.CodeGen.render_parameter(:elixir, parameter) %><% end + %><%= for parameter <- action.required_request_header_parameters do %> + <%= AWS.CodeGen.render_parameter(:elixir, parameter) %><% end + %><%= if action.send_body_as_binary? do %> + * `:input` (`t:binary<%= if not action.body_required? do %> | nil<% end %>`<%= if action.body_required? do %> required<% end %>) + <% else %><%= if action.has_body? do %> + * `:input` (`t:map<%= if not action.body_required? do %> | nil<% end %>`<% if action.body_required? do %> required<% end %>):<%= for parameter <- action.required_body_parameters do %> + <%= AWS.CodeGen.render_body_parameter(parameter) %><% end + %><%= for parameter <- action.optional_body_parameters do %> + <%= AWS.CodeGen.render_body_parameter(parameter) %><% end + %><% end + %><% end + %><%= AWS.CodeGen.ElixirHelpers.render_keyword_parameters(action) %> + """}) + def render_docstring(%RestService.Action{} = action, _context, _types) do + {res, _} = Code.eval_quoted(@docstring_rest_quoted, action: action) + res + end + + @keyword_params EEx.compile_string(~s{ + + ## Keyword parameters:<%= for parameter <- action.optional_query_parameters do %> + <%= AWS.CodeGen.render_parameter(:elixir, parameter) %><% end + %><%= for parameter <- action.optional_request_header_parameters do %> + <%= AWS.CodeGen.render_parameter(:elixir, parameter) %><% end %>}) + def render_keyword_parameters(action) do + if Enum.empty?(action.optional_query_parameters) and Enum.empty?(action.optional_request_header_parameters) do + "" + else + {res, _} = Code.eval_quoted(@keyword_params, action: action) + res + end + end + + @docstring_post_quoted EEx.compile_string(~s/@doc """ + <%= if String.trim(action.docstring) != "" do %> + <%= action.docstring %><% end %> + + [API Reference](<%= action.docs_url %>) + + ## Parameters: + * `:input` (`t:<%= input_type %>`<% if action.body_required? do %> required<% end %>)<%= if not is_nil(type_fields) do %> + %{ + <%= AWS.CodeGen.ElixirHelpers.render_type_fields(input_type, type_fields, 9) %> + }<% end %> + """/) + def render_docstring(%PostService.Action{} = action, context, types) do + input_type = + AWS.CodeGen.Types.function_argument_type(:elixir, action) + # TODO: This is dirty. + |> then(fn x -> + if String.contains?(x, "(") do + x + |> String.split("(") + |> hd() + else + x + end + end) + + type_fields = + types[input_type] + + {res, _} = + Code.eval_quoted(@docstring_post_quoted, + action: action, + input_type: input_type, + type_fields: type_fields + ) + + res + end + + def render_guards(action) do + required_params = + action.required_query_parameters ++ action.required_request_header_parameters + + body_guard = + cond do + action.send_body_as_binary? and action.body_required? -> + "is_binary(input)" + + action.send_body_as_binary? and not action.body_required? -> + "is_binary(input) or is_nil(input)" + + action.has_body? and action.body_required? -> + "is_map(input)" + + action.has_body? and not action.body_required? -> + "is_map(input) or is_nil(input)" + + true -> + "" + end + + req_guards = + required_params + |> Enum.map(fn param -> + case param.type do + "string" -> + "is_binary(#{param.code_name})" + + "integer" -> + "is_integer(#{param.code_name})" + + "long" -> + "is_integer(#{param.code_name})" + + "boolean" -> + "is_boolean(#{param.code_name})" + + "list[" <> _ -> + "is_binary(#{param.code_name})" + + "enum[" <> _ -> + "is_binary(#{param.code_name})" + + "timestamp" <> _ -> + "is_binary(#{param.code_name})" + + nil -> + raise "UNKNOWN TYPE" + end + end) + |> Enum.join(" and ") + + cond do + body_guard == "" and req_guards == "" -> + "" + + body_guard == "" -> + "when " <> req_guards + + req_guards == "" -> + "when " <> body_guard + + true -> + "when (" <> body_guard <> ") and " <> req_guards + end + end + + def maybe_render_stage(context) do + if context.module_name == "AWS.ApiGatewayManagementApi" do + ", stage" + else + "" + end + end + + def maybe_render_stage_spec(context) do + if context.module_name == "AWS.ApiGatewayManagementApi" do + ", any()" + else + "" + end + end + + @render_spec_get EEx.compile_string( + ~s/@spec <%= action.function_name %>(AWS.Client.t()<%= maybe_stage + %><%= required_param_types %>, Keyword.t()) :: <%= AWS.CodeGen.Types.return_type(context.language, action) + %>/) + def render_spec(:get, context, action) do + maybe_stage = maybe_render_stage_spec(context) + required_param_types = AWS.CodeGen.Types.required_function_parameter_types(action) + + {res, _} = + Code.eval_quoted(@render_spec_get, + action: action, + context: context, + maybe_stage: maybe_stage, + required_param_types: required_param_types + ) + + res + end + + @render_spec_other EEx.compile_string( + ~s/@spec <%= action.function_name %>(AWS.Client.t()<%= maybe_stage + %><%= required_param_types %><%= body_type %>, Keyword.t()) :: <%= AWS.CodeGen.Types.return_type(context.language, action) + %>/) + def render_spec(_, context, action) do + maybe_stage = maybe_render_stage_spec(context) + + required_param_types = AWS.CodeGen.Types.required_function_parameter_types(action) + + body_type = + cond do + action.send_body_as_binary? and action.body_required? -> + ", input :: binary()" + + action.send_body_as_binary? and not action.body_required? -> + ", input :: binary() | nil" + + action.has_body? and action.body_required? -> + ", input :: map()" + + action.has_body? and not action.body_required? -> + ", input :: map() | nil" + + true -> + "" + end + + {res, _} = + Code.eval_quoted(@render_spec_other, + action: action, + context: context, + body_type: body_type, + maybe_stage: maybe_stage, + required_param_types: required_param_types + ) + + res + end + + @render_def EEx.compile_string( +~s/def <%= action.function_name %>(%Client{} = client <%= maybe_stage +%><%= required_params +%>, <%= if action.has_body?, do: "input,", else: "" %> options \\\\ []) <%= AWS.CodeGen.ElixirHelpers.render_guards(action) %> + /) + def render_def(context, action) do + maybe_stage = maybe_render_stage(context) + + required_params = AWS.CodeGen.RestService.required_function_parameters(action) + + {res, _} = + Code.eval_quoted(@render_def, + action: action, + context: context, + maybe_stage: maybe_stage, + required_params: required_params + ) + + res + end +end diff --git a/lib/aws_codegen/post_service.ex b/lib/aws_codegen/post_service.ex index bcdf314..24d80b4 100644 --- a/lib/aws_codegen/post_service.ex +++ b/lib/aws_codegen/post_service.ex @@ -6,12 +6,29 @@ defmodule AWS.CodeGen.PostService do defmodule Action do defstruct arity: nil, docstring: nil, + docs_url: nil, function_name: nil, input: nil, output: nil, + url_parameters: [], + has_body?: true, + body_required?: true, + body_parameters: [], + required_body_parameters: [], + optional_body_parameters: [], + query_parameters: [], + required_query_parameters: [], + optional_query_parameters: [], + request_header_parameters: [], + request_headers_parameters: [], + required_request_header_parameters: [], + optional_request_header_parameters: [], errors: %{}, host_prefix: nil, - name: nil + name: nil, + send_body_as_binary?: false, + language: nil, + method: :post end @configuration %{ @@ -126,6 +143,16 @@ defmodule AWS.CodeGen.PostService do end end + defp collect_params(language, api_spec, operation) do + AWS.CodeGen.RestService.collect_parameters( + language, + api_spec, + operation, + "members", + "smithy.api#input" + ) + end + defp collect_actions(language, api_spec) do shapes = api_spec["shapes"] @@ -159,6 +186,14 @@ defmodule AWS.CodeGen.PostService do Enum.map(operations, fn operation -> operation_spec = shapes[operation] + # The AWS Docs sometimes use an arbitrary service name, so we cannot build direct urls. Instead we just link to a search + docs_url = Docstring.docs_url(shapes, operation) + + # input_shape = Shapes.get_input_shape(operation_spec) + + params = + collect_params(language, api_spec, operation) + %Action{ arity: 3, docstring: @@ -166,12 +201,14 @@ defmodule AWS.CodeGen.PostService do language, operation_spec["traits"]["smithy.api#documentation"] ), + docs_url: docs_url, 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\.[^#]+#/, ""), input: operation_spec["input"], output: operation_spec["output"], - errors: operation_spec["errors"] + errors: operation_spec["errors"], + language: language } end) |> Enum.sort(fn a, b -> a.function_name < b.function_name end) diff --git a/lib/aws_codegen/rest_service.ex b/lib/aws_codegen/rest_service.ex index 02d2e50..47afd55 100644 --- a/lib/aws_codegen/rest_service.ex +++ b/lib/aws_codegen/rest_service.ex @@ -8,17 +8,26 @@ defmodule AWS.CodeGen.RestService do defstruct arity: nil, docstring: nil, + docstring_header: nil, + docs_url: nil, method: nil, request_uri: nil, success_status_code: nil, function_name: nil, name: nil, url_parameters: [], + has_body?: false, + body_required?: false, + body_parameters: [], + required_body_parameters: [], + optional_body_parameters: [], query_parameters: [], required_query_parameters: [], + optional_query_parameters: [], request_header_parameters: [], request_headers_parameters: [], required_request_header_parameters: [], + optional_request_header_parameters: [], response_header_parameters: [], send_body_as_binary?: false, receive_body_as_binary?: false, @@ -76,7 +85,9 @@ defmodule AWS.CodeGen.RestService do defstruct code_name: nil, name: nil, location_name: nil, - required: false + required: false, + type: nil, + docs: nil def multi_segment?(parameter, request_uri) do {:ok, re} = Regex.compile("{#{parameter.location_name}\\+}") @@ -198,7 +209,20 @@ defmodule AWS.CodeGen.RestService do end _ -> - [] + case required_only do + false -> + [ + join_parameters(action.query_parameters, language), + join_parameters(action.request_header_parameters, language), + join_parameters(action.request_headers_parameters, language) + ] + + true -> + [ + join_parameters(action.required_query_parameters, language), + join_parameters(action.required_request_header_parameters, language) + ] + end end ]) end @@ -252,13 +276,26 @@ defmodule AWS.CodeGen.RestService do operation_spec = shapes[operation] request_uri = operation_spec["traits"]["smithy.api#http"]["uri"] url_parameters = collect_url_parameters(language, api_spec, operation) + + body_parameters = + collect_body_parameters(language, api_spec, operation) + + body_required? = get_body_required?(api_spec, operation_spec) + query_parameters = collect_query_parameters(language, api_spec, operation) function_name = AWS.CodeGen.Name.to_snake_case(operation) request_header_parameters = collect_request_header_parameters(language, api_spec, operation) - is_required = fn param -> param.required end - required_query_parameters = Enum.filter(query_parameters, is_required) - required_request_header_parameters = Enum.filter(request_header_parameters, is_required) + # The AWS Docs sometimes use an arbitrary service name, so we cannot build direct urls. Instead we just link to a search + docs_url = Docstring.docs_url(shapes, operation) + + {required_query_params, opt_query_params} = split_parameters(query_parameters) + + {required_request_header_params, opt_request_header_params} = + split_parameters(request_header_parameters) + + {required_body_params, opt_body_params} = split_parameters(body_parameters) + method = operation_spec["traits"]["smithy.api#http"]["method"] len_for_method = @@ -266,10 +303,10 @@ defmodule AWS.CodeGen.RestService do "GET" -> case language do :elixir -> - 2 + length(request_header_parameters) + length(query_parameters) + 2 + length(required_request_header_params) + length(required_query_params) :erlang -> - 4 + length(required_request_header_parameters) + length(required_query_parameters) + 4 + length(required_request_header_params) + length(required_query_params) end _ -> @@ -279,13 +316,19 @@ defmodule AWS.CodeGen.RestService do input_shape = Shapes.get_input_shape(operation_spec) output_shape = Shapes.get_output_shape(operation_spec) + docstring = + Docstring.format( + language, + operation_spec["traits"]["smithy.api#documentation"] + ) + + has_body? = method != "GET" and not Enum.empty?(body_parameters) + send_body_as_binary? = Shapes.body_as_binary?(shapes, input_shape) + %Action{ arity: length(url_parameters) + len_for_method, - docstring: - Docstring.format( - language, - operation_spec["traits"]["smithy.api#documentation"] - ), + docstring: docstring, + docs_url: docs_url, method: method, request_uri: request_uri, success_status_code: success_status_code(operation_spec), @@ -293,12 +336,19 @@ defmodule AWS.CodeGen.RestService do name: operation, url_parameters: url_parameters, query_parameters: query_parameters, - required_query_parameters: required_query_parameters, + body_parameters: body_parameters, + required_body_parameters: required_body_params, + optional_body_parameters: opt_body_params, + has_body?: has_body?, + body_required?: body_required?, + required_query_parameters: required_query_params, + optional_query_parameters: opt_query_params, request_header_parameters: request_header_parameters, - required_request_header_parameters: required_request_header_parameters, + required_request_header_parameters: required_request_header_params, + optional_request_header_parameters: opt_request_header_params, response_header_parameters: collect_response_header_parameters(language, api_spec, operation), - send_body_as_binary?: Shapes.body_as_binary?(shapes, input_shape), + send_body_as_binary?: send_body_as_binary?, receive_body_as_binary?: Shapes.body_as_binary?(shapes, output_shape), host_prefix: operation_spec["traits"]["smithy.api#endpoint"]["hostPrefix"], language: language, @@ -326,6 +376,15 @@ defmodule AWS.CodeGen.RestService do url_params end + defp collect_body_parameters(language, api_spec, operation) do + [ + collect_parameters(language, api_spec, operation, "input", "smithy.api#httpPayload"), + collect_parameters(language, api_spec, operation, "input", "smithy.api#jsonName") + # collect_parameters(language, api_spec, operation, "members", "smithy.api#jsonName") + ] + |> Enum.concat() + end + defp collect_query_parameters(language, api_spec, operation) do query_params = collect_parameters(language, api_spec, operation, "input", "smithy.api#httpQueryParams") @@ -334,6 +393,11 @@ defmodule AWS.CodeGen.RestService do query_params ++ params end + @spec split_parameters(any()) :: {list(any), list(any)} + def split_parameters(params) do + Enum.split_with(params, & &1.required) + end + defp collect_request_header_parameters(language, api_spec, operation) do collect_parameters(language, api_spec, operation, "input", "smithy.api#httpHeader") end @@ -342,7 +406,7 @@ defmodule AWS.CodeGen.RestService do collect_parameters(language, api_spec, operation, "output", "smithy.api#httpHeader") end - defp collect_parameters(language, api_spec, operation, data_type, param_type) do + def collect_parameters(language, api_spec, operation, data_type, param_type) do shape_name = api_spec["shapes"][operation][data_type]["target"] if shape_name do @@ -360,7 +424,31 @@ defmodule AWS.CodeGen.RestService do |> Enum.filter(filter_fn(param_type)) |> Enum.map(fn {name, x} -> required = Enum.member?(required_members, name) - build_parameter(language, {name, x["traits"][param_type]}, required) + + tynfo = + get_type_info(x, api_spec) + + docs = get_in(x, ["traits", "smithy.api#documentation"]) + + docs = + if is_nil(docs) do + "" + else + extract_param_docs_snippet(docs) + end + + if is_nil(tynfo) do + build_parameter(language, {name, x["traits"][param_type]}, required, "string", docs) + else + traits = x["traits"] + xml_name = traits["smithy.api#xmlName"] + json_name = traits["smithy.api#jsonName"] + + # If the parameter is a body parameter use the xml/json name instead of the smithy name. + location_name = xml_name || json_name || x["traits"][param_type] + + build_parameter(language, {name, location_name}, required, tynfo, docs) + end end) end else @@ -368,13 +456,136 @@ defmodule AWS.CodeGen.RestService do end end + def get_body_required?(api_spec, operation_spec) do + body_req_name = get_in(operation_spec, ["input", "target"]) + + if is_nil(body_req_name) do + false + else + members = api_spec["shapes"][body_req_name]["members"] + + if is_nil(members) do + false + else + payloads = + members + |> Enum.filter(filter_fn("smithy.api#httpPayload")) + + # This is necessary to detect e.g. APIGatewayV2 create_api/3 input as + # required. + jsons = + members + |> Enum.filter(filter_fn("smithy.api#jsonName")) + + Enum.concat(payloads, jsons) + |> Enum.map(fn {_name, x} -> + required? = get_in(x, ["traits", "smithy.api#required"]) + default = get_in(x, ["traits", "smithy.api#default"]) + + required? || default == "" + end) + |> Enum.any?() + end + end + end + + def extract_param_docs_snippet(docs) do + case Floki.parse_fragment(docs) do + {:ok, [{"p", _attrs, inner_content} | _rest]} -> + inner_content + |> sanitize_html() + |> Floki.text() + + {:ok, [first_node | _rest]} -> + first_node + |> sanitize_html() + |> Floki.text() + + {:error, _} -> + "" + end + end + + def sanitize_html(tree) do + tree + # NOTE: This doesn't work, because it only updates the inner part of the tag. + # |> Floki.find_and_update("code", fn + # {"code", inner} -> + # "`#{inner}`" + # + # other -> + # IO.inspect(other) + # other + # end) + |> Floki.find_and_update("p", fn + {"p", inner} -> + inner + + other -> + other + end) + end + + def get_type_info(x, api_spec) do + t = x["target"] + type = api_spec["shapes"][t] + + # if is_nil(type) do + # # THIS IS SOMETIMES INCORRECT! t is not guaranteed to exist in api_spec["shapes"]... + # dbg({x, t, type}) + # end + + build_type_details(type, api_spec) + end + + def build_type_details(type, _api_spec) when is_binary(type) do + type + end + + # If the timestamp has traits such as `http-date`, or `date-time` include them. + def build_type_details(%{"type" => "timestamp", "traits" => _} = type, _api_spec) do + fmt = type["traits"]["smithy.api#timestampFormat"] + + "timestamp[#{fmt}]" + end + + def build_type_details(%{"type" => "enum"} = type, _api_spec) do + keys = + type["members"] + |> Map.keys() + + ~s/enum["#{Enum.join(keys, "|")}"]/ + end + + def build_type_details(%{"type" => "list"} = type, api_spec) do + deets = + type["member"]["target"] + |> build_type_details(api_spec) + + "list[#{deets}]" + end + + # TODO: Should raise here on unknown types, but handling it elsewhere for now. + # def build_type_details(nil, api_spec) do + # "string" + # end + + def build_type_details(type, _api_spec) do + type["type"] + end + defp filter_fn(location) do fn {_name, member_spec} -> not is_nil(member_spec["traits"][location]) end end - defp build_parameter(language, {name, %{}}, required) do + defp build_parameter(_, a, required, nil, docs) do + dbg() + raise "build_parameter type is nil" + end + + defp build_parameter(language, {name, %{}}, required, type, docs) do %Parameter{ code_name: if language == :elixir do @@ -384,11 +595,13 @@ defmodule AWS.CodeGen.RestService do end, name: name, location_name: name, - required: required + required: required, + type: type, + docs: docs } end - defp build_parameter(language, {name, data}, required) do + defp build_parameter(language, {name, data}, required, type, docs) do %Parameter{ code_name: if language == :elixir do @@ -398,7 +611,9 @@ defmodule AWS.CodeGen.RestService do end, name: name, location_name: data, - required: required + required: required, + type: type, + docs: docs } end end diff --git a/lib/aws_codegen/types.ex b/lib/aws_codegen/types.ex index dba1bb9..b2d3e4e 100644 --- a/lib/aws_codegen/types.ex +++ b/lib/aws_codegen/types.ex @@ -17,6 +17,7 @@ defmodule AWS.CodeGen.Types do defp process_structure_shape(context, shape, acc) do type = normalize_type_name(shape.name) + types = process_shape_members(context, shape) update_acc_with_types(acc, type, types, context) end @@ -194,7 +195,8 @@ defmodule AWS.CodeGen.Types do end defp reserved_type(type) do - type == "node" || type == "term" || type == "function" || type == "reference" + type == "node" || type == "term" || type == "function" || type == "reference" || + type == "identifier" end def function_argument_type(:elixir, action) do @@ -302,7 +304,12 @@ defmodule AWS.CodeGen.Types do def function_parameter_types(_method, action, _required_only) do language = action.language - join_parameter_types(action.url_parameters, 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 defp join_parameter_types(parameters, :elixir) do @@ -310,8 +317,10 @@ defmodule AWS.CodeGen.Types do parameters, fn parameter -> if not parameter.required do + # TODO: map the types to elixir types here: ", #{parameter.type} | nil" ", String.t() | nil" else + # TODO: map the types to elixir types here: ", #{parameter.type}" ", String.t()" end end diff --git a/mix.exs b/mix.exs index 60d3721..bc63673 100644 --- a/mix.exs +++ b/mix.exs @@ -25,6 +25,7 @@ defmodule AWS.CodeGen.Mixfile do [ {:earmark, "~> 1.4", only: :dev}, {:ex_doc, "~> 0.31.1", only: :dev}, + {:excribe, "~> 0.1.1", only: :dev}, {:floki, "~> 0.35"}, {:fast_html, "~> 2.3"}, {:poison, "~> 4.0 or ~> 5.0"} diff --git a/mix.lock b/mix.lock index 75a2f5d..aeb3876 100644 --- a/mix.lock +++ b/mix.lock @@ -3,6 +3,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "excribe": {:hex, :excribe, "0.1.1", "3276fb00c2dd2928e06a29521a9b78934de8929f5a8081de39510d7df5a3c7ac", [:mix], [], "hexpm", "e6b26840f340cb20e6dbf774c556a6cbd4e8a3aec3a34f366749ba6d98dba3e3"}, "fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"}, "floki": {:hex, :floki, "0.35.3", "0c8c6234aa71cb2b069cf801e8f8f30f8d096eb452c3dae2ccc409510ec32720", [:mix], [], "hexpm", "6d9f07f3fc76599f3b66c39f4a81ac62c8f4d9631140268db92aacad5d0e56d4"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, diff --git a/priv/post.ex.eex b/priv/post.ex.eex index f839641..a90d22d 100644 --- a/priv/post.ex.eex +++ b/priv/post.ex.eex @@ -11,17 +11,17 @@ defmodule <%= context.module_name %> do alias AWS.Client alias AWS.Request - <%= for {type_name, type_fields} <- AWS.CodeGen.Types.types(context) do %> +<% types = AWS.CodeGen.Types.types(context) %> + +<%= for {type_name, type_fields} <- types do %> @typedoc """ ## Example: <%= if map_size(type_fields) == 0 do %> - <%= "#{type_name}() :: %{}" %> + <%= type_name %>() :: %{} <% else %> - <%= "#{type_name}() :: %{" %> - <%= Enum.map_join(type_fields, ",\n ", fn {field_name, field_type} -> - ~s{ #{field_name}#{field_type}} - end) %> + <%= type_name %>() :: %{ + <%= AWS.CodeGen.ElixirHelpers.render_type_fields(type_name, type_fields, 6) %> } <% end %> """ @@ -33,7 +33,7 @@ defmodule <%= context.module_name %> do errors = action.errors if not is_nil(errors) do errors_snakecased = Enum.map(errors, fn error -> AWS.CodeGen.Name.to_snake_case(String.replace(error["target"], ~r/com\.amazonaws\.[^#]+#/, "")) end) - all_types = AWS.CodeGen.Types.types(context) + all_types = types error_types = Enum.reduce(all_types, [], fn {type_name, _type_fields}, acc -> @@ -67,12 +67,9 @@ defmodule <%= context.module_name %> do } end <%= for action <- context.actions do %> - <%= if String.trim(action.docstring) != "" do %> - @doc """ -<%= action.docstring %> - """<% end %> - @spec <%= action.function_name %>(map(), <%= AWS.CodeGen.Types.function_argument_type(context.language, action)%>, list()) :: <%= AWS.CodeGen.Types.return_type(context.language, action)%> - def <%= action.function_name %>(%Client{} = client, input, options \\ []) do + <%= AWS.CodeGen.ElixirHelpers.render_docstring(action, context, types) %> + <%= AWS.CodeGen.ElixirHelpers.render_spec(:other, context, action) %> + <%= AWS.CodeGen.ElixirHelpers.render_def(context, action) %> do meta = <%= if action.host_prefix do %> metadata() |> Map.put_new(:host_prefix, <%= inspect(action.host_prefix) %>) diff --git a/priv/rest.ex.eex b/priv/rest.ex.eex index 0bf88e3..022f248 100644 --- a/priv/rest.ex.eex +++ b/priv/rest.ex.eex @@ -11,19 +11,19 @@ defmodule <%= context.module_name %> do alias AWS.Client alias AWS.Request - <%= for {type_name, type_fields} <- AWS.CodeGen.Types.types(context) do %> +<% types = AWS.CodeGen.Types.types(context) %> + +<%= for {type_name, type_fields} <- types do %> @typedoc """ ## Example: -<%= if Enum.empty?(type_fields) do %> - <%= "#{type_name}() :: %{}" %> -<% else %> - <%= "#{type_name}() :: %{" %> - <%= Enum.map_join(type_fields, ",\n ", fn {field_name, field_type} -> - ~s{ #{field_name}#{field_type}} - end) %> + <%= if Enum.empty?(type_fields) do %> + <%= type_name %>() :: %{} + <% else %> + <%= type_name %>() :: %{ + <%= AWS.CodeGen.ElixirHelpers.render_type_fields(type_name, type_fields, 6) %> } -<% end %> + <% end %> """ @type <%= if Enum.empty?(type_fields) do "#{type_name}() :: %{}" else "#{type_name}() :: %{String.t => any()}" end %> <% end %> @@ -67,22 +67,30 @@ defmodule <%= context.module_name %> do } end<%= for action <- context.actions do %> - <%= if String.trim(action.docstring) != "" do %> - @doc """ -<%= action.docstring %> - """<% end %><%= if action.method == "GET" do %> - @spec <%= action.function_name %>(map()<%= if context.module_name == "AWS.ApiGatewayManagementApi" do %>, String.t()<% end %><%= AWS.CodeGen.Types.function_parameter_types(action.method, action, false)%>, list()) :: <%= AWS.CodeGen.Types.return_type(context.language, action)%> - def <%= action.function_name %>(%Client{} = client<%= if context.module_name == "AWS.ApiGatewayManagementApi" do %>, stage<% end %><%= AWS.CodeGen.RestService.function_parameters(action) %>, options \\ []) do +<%= if action.method == "GET" do %> + <%= AWS.CodeGen.ElixirHelpers.render_docstring(action, context, types) %> + <%= AWS.CodeGen.ElixirHelpers.render_spec(:get, context, action) %> + <%= AWS.CodeGen.ElixirHelpers.render_def(context, action) %> do url_path = "<%= if context.module_name == "AWS.ApiGatewayManagementApi" do %>/#{stage}<% end %><%= AWS.CodeGen.RestService.Action.url_path(action) %>" - headers = []<%= for parameter <- action.request_header_parameters do %> - headers = if !is_nil(<%= parameter.code_name %>) do - [{"<%= parameter.location_name %>", <%= parameter.code_name %>} | headers] + + <%= AWS.CodeGen.ElixirHelpers.define_and_validate_optionals(action) %> + + # Required headers + headers = [<%= action.required_request_header_parameters |> Enum.map(fn parameter -> ~s|{"#{parameter.location_name}", #{parameter.code_name}}| end) |> Enum.join(",") %>] + + # Optional headers<%= for parameter <- Enum.reverse(action.optional_request_header_parameters) do %> + headers = if opt_val = Keyword.get(options, :<%= parameter.code_name %>) do + [{"<%= parameter.location_name %>", opt_val} | headers] else headers end<% end %> - query_params = []<%= for parameter <- Enum.reverse(action.query_parameters) do %> - query_params = if !is_nil(<%= parameter.code_name %>) do - [{"<%= parameter.location_name %>", <%= parameter.code_name %>} | query_params] + + # Required query params + query_params = [<%= action.required_query_parameters |> Enum.map(fn parameter -> ~s|{"#{parameter.location_name}", #{parameter.code_name}}| end) |> Enum.join(",") %>] + + # Optional query params<%= for parameter <- Enum.reverse(action.optional_query_parameters) do %> + query_params = if opt_val = Keyword.get(options, :<%= parameter.code_name %>) do + [{"<%= parameter.location_name %>", opt_val} | query_params] else query_params end<% end %><%= if length(action.response_header_parameters) > 0 do %> @@ -115,22 +123,37 @@ defmodule <%= context.module_name %> do metadata() <% end %> - Request.request_rest(client, meta, :get, url_path, query_params, headers, nil, options, <%= inspect(action.success_status_code) %>)<% else %> -@spec <%= action.function_name %>(map()<%= AWS.CodeGen.Types.function_parameter_types(action.method, action, false)%>, <%= if context.module_name == "AWS.ApiGatewayManagementApi" do %> String.t(), <% end %><%= AWS.CodeGen.Types.function_argument_type(context.language, action)%>, list()) :: <%= AWS.CodeGen.Types.return_type(context.language, action)%> -def <%= action.function_name %>(%Client{} = client<%= AWS.CodeGen.RestService.function_parameters(action) %>, <%= if context.module_name == "AWS.ApiGatewayManagementApi" do %> stage, <% end %>input, options \\ []) do - url_path = "<%= if context.module_name == "AWS.ApiGatewayManagementApi" do %>/#{stage}<% end %><%= AWS.CodeGen.RestService.Action.url_path(action) %>"<%= if length(action.request_header_parameters) > 0 do %> - {headers, input} = - [<%= for parameter <- action.request_header_parameters do %> - {"<%= parameter.name %>", "<%= parameter.location_name %>"},<% end %> - ] - |> Request.build_params(input)<% else %> - headers = []<% end %><%= if length(action.query_parameters) > 0 do %> - {query_params, input} = - [<%= for parameter <- action.query_parameters do %> - {"<%= parameter.name %>", "<%= parameter.location_name %>"},<% end %> - ] - |> Request.build_params(input)<% else %> - query_params = []<% end %><%= if length(action.response_header_parameters) > 0 do %> + <%= AWS.CodeGen.ElixirHelpers.drop_optionals(action) %> + + Request.request_rest(client, meta, :get, url_path, query_params, headers, nil, options, <%= inspect(action.success_status_code) %>)<% +else %> + + <%= AWS.CodeGen.ElixirHelpers.render_docstring(action, context, types) %> + <%= AWS.CodeGen.ElixirHelpers.render_spec(:other, context, action) %> + <%= AWS.CodeGen.ElixirHelpers.render_def(context, action) %> do + url_path = "<%= if context.module_name == ~s(AWS.ApiGatewayManagementApi) do %>/#{stage}<% end %><%= AWS.CodeGen.RestService.Action.url_path(action) %>" + + <%= AWS.CodeGen.ElixirHelpers.define_and_validate_optionals(action) %> + + # Required headers + headers = [<%= action.required_request_header_parameters |> Enum.map(fn parameter -> ~s|{"#{parameter.location_name}", #{parameter.code_name}}| end) |> Enum.join(",") %>] + + # Optional headers<%= for parameter <- Enum.reverse(action.optional_request_header_parameters) do %> + headers = if opt_val = Keyword.get(options, :<%= parameter.code_name %>) do + [{"<%= parameter.location_name %>", opt_val} | headers] + else + headers + end<% end %> + + # Required query params + query_params = [<%= action.required_query_parameters |> Enum.map(fn parameter -> ~s|{"#{parameter.location_name}", #{parameter.code_name}}| end) |> Enum.join(",") %>] + + # Optional query params<%= for parameter <- Enum.reverse(action.optional_query_parameters) do %> + query_params = if opt_val = Keyword.get(options, :<%= parameter.code_name %>) do + [{"<%= parameter.location_name %>", opt_val} | query_params] + else + query_params + end<% end %><%= if length(action.response_header_parameters) > 0 do %> options = Keyword.put( options, :response_header_parameters, @@ -168,6 +191,10 @@ def <%= action.function_name %>(%Client{} = client<%= AWS.CodeGen.RestService.fu metadata() <% end %> - Request.request_rest(client, meta, <%= AWS.CodeGen.RestService.Action.method(action) %>, url_path, query_params, headers, input, options, <%= inspect(action.success_status_code) %>)<% end %> + <%= AWS.CodeGen.ElixirHelpers.drop_optionals(action) %> + + body = <%= if action.has_body? do %>input<% else %>nil<% end %> + + Request.request_rest(client, meta, <%= AWS.CodeGen.RestService.Action.method(action) %>, url_path, query_params, headers, body, options, <%= inspect(action.success_status_code) %>)<% end %> end<% end %> end