Skip to content

Commit

Permalink
Generate type specs for aws-erlang. Does not pass Dialyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
onno-vos-dev committed Mar 12, 2024
1 parent c43e47b commit 92118ae
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 42 deletions.
5 changes: 2 additions & 3 deletions lib/aws_codegen/post_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,17 +177,16 @@ defmodule AWS.CodeGen.PostService do
members: shape["members"],
min: shape["min"],
enum: shape["enum"],
is_input: is_input?(name, shape)
is_input: is_input?(shape)
}}
end)
|> Enum.into(%{})
end

defp is_input?(name, shape) do
defp is_input?(shape) do
if Map.has_key?(shape, "traits") do
traits = shape["traits"]
if Map.has_key?(traits, "smithy.api#input") do
IO.puts("Found shape: #{name} with input: true!!!")
true
else
false
Expand Down
82 changes: 49 additions & 33 deletions lib/aws_codegen/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,103 +4,119 @@ defmodule AWS.CodeGen.Types do

# Unfortunately, gotta patch over auto-defining types that already exist in Elixir

def shape_to_type("String", _) do
def shape_to_type(:elixir, "String", _) do
"String.t()"
end
def shape_to_type(:erlang, "String", _) do
"string()"
end

def shape_to_type("string", _) do
def shape_to_type(:elixir, "string", _) do
"String.t()"
end
def shape_to_type(:erlang, "string", _) do
"string()"
end

def shape_to_type("Identifier", _) do
def shape_to_type(:elixir, "Identifier", _) do
"String.t()"
end
def shape_to_type(:erlang, "Identifier", _) do
"string()"
end

def shape_to_type("identifier", _) do
def shape_to_type(:elixir, "identifier", _) do
"String.t()"
end
def shape_to_type(:erlang, "identifier", _) do
"string()"
end

def shape_to_type("XmlString" <> _rest, _) do
def shape_to_type(:elixir, "XmlString" <> _rest, _) do
"String.t()"
end
def shape_to_type(:erlang, "XmlString" <> _rest, _) do
"string"
end

def shape_to_type("NullablePositiveInteger", _) do
def shape_to_type(:elixir, "NullablePositiveInteger", _) do
"nil | non_neg_integer()"
end
def shape_to_type(:erlang, "NullablePositiveInteger", _) do
"undefined | non_neg_integer()"
end

def shape_to_type(%Shape{type: type}, _module_name) when type in ["float", "double", "long"] do
def shape_to_type(_, %Shape{type: type}, _module_name) when type in ["float", "double", "long"] do
"float()"
end

def shape_to_type(%Shape{type: "timestamp"}, _module_name) do
def shape_to_type(_, %Shape{type: "timestamp"}, _module_name) do
"non_neg_integer()"
end

def shape_to_type(%Shape{type: "map"}, _module_name) do
def shape_to_type(_, %Shape{type: "map"}, _module_name) do
"map()"
end

def shape_to_type(%Shape{type: "blob"}, _module_name) do
def shape_to_type(_, %Shape{type: "blob"}, _module_name) do
"binary()"
end

def shape_to_type(%Shape{type: "string"}, _module_name) do
def shape_to_type(:elixir, %Shape{type: "string"}, _module_name) do
"String.t()"
end
def shape_to_type(:erlang, %Shape{type: "string"}, _module_name) do
"string()"
end

def shape_to_type(%Shape{type: "integer"}, _module_name) do
def shape_to_type(_, %Shape{type: "integer"}, _module_name) do
"integer()"
end

def shape_to_type(%Shape{type: "boolean"}, _module_name) do
def shape_to_type(_, %Shape{type: "boolean"}, _module_name) do
"boolean()"
end

def shape_to_type(%Shape{type: "enum"}, _module_name) do
def shape_to_type(_, %Shape{type: "enum"}, _module_name) do
"list(any())"
end

def shape_to_type(%Shape{type: "union"}, _module_name) do
def shape_to_type(_, %Shape{type: "union"}, _module_name) do
"list()"
end

def shape_to_type(%Shape{type: "document"}, _module_name) do
def shape_to_type(_, %Shape{type: "document"}, _module_name) do
"any()"
end

def shape_to_type(%Shape{name: shape_name} = shape, module_name) do
raise "missing shape for type #{shape_name} #{inspect(shape, limit: :infinity)}"
"#{module_name}.#{Name.to_snake_case(shape_name)}"
end

def shape_to_type(context, shape_name, module_name, all_shapes) do
case shape_name do
"smithy.api#String" ->
"[#{shape_to_type(%Shape{type: "string"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "string"}, module_name)}]"

"smithy.api#Integer" ->
"[#{shape_to_type(%Shape{type: "integer"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "integer"}, module_name)}]"

"smithy.api#Timestamp" ->
"[#{shape_to_type(%Shape{type: "timestamp"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "timestamp"}, module_name)}]"

"smithy.api#PrimitiveLong" ->
"[#{shape_to_type(%Shape{type: "long"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "long"}, module_name)}]"

"smithy.api#Long" ->
"[#{shape_to_type(%Shape{type: "long"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "long"}, module_name)}]"

"smithy.api#Boolean" ->
"[#{shape_to_type(%Shape{type: "boolean"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "boolean"}, module_name)}]"

"smithy.api#PrimitiveBoolean" ->
"[#{shape_to_type(%Shape{type: "boolean"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "boolean"}, module_name)}]"

"smithy.api#Double" ->
"[#{shape_to_type(%Shape{type: "double"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "double"}, module_name)}]"

"smithy.api#Document" ->
"[#{shape_to_type(%Shape{type: "document"}, module_name)}]"
"[#{shape_to_type(context.language, %Shape{type: "document"}, module_name)}]"

"smithy.api#Unit" ->
"[]"
Expand All @@ -110,15 +126,15 @@ defmodule AWS.CodeGen.Types 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.", ""))}_#{type}()"
"#{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.", ""))}_#{type}())"
"list(#{String.downcase(String.replace(context.module_name, ["aws_", "AWS."], ""))}_#{type}())"
else
"list(#{type}())"
end
Expand All @@ -127,7 +143,7 @@ defmodule AWS.CodeGen.Types do
raise "Tried to reference an undefined shape for #{shape_name}"

shape ->
shape_to_type(shape, module_name)
shape_to_type(context.language, shape, module_name)
end
end
end
Expand Down
40 changes: 35 additions & 5 deletions lib/aws_codegen/util.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
defmodule AWS.CodeGen.Util do

alias AWS.CodeGen.PostService.Shape

def service_docs(service) do
with "<p></p>" <- service["traits"]["smithy.api#documentation"], do: ""
end
Expand Down Expand Up @@ -66,7 +64,7 @@ defmodule AWS.CodeGen.Util do
fn {_name, shape_member}, a ->
target = shape_member["target"]
shape_member_type = AWS.CodeGen.Types.shape_to_type(context, target, context.module_name, context.shapes)
Map.put(a, is_required(shape.is_input, shape_member, target), shape_member_type)
Map.put(a, is_required(context.language, shape.is_input, shape_member, target), shape_member_type)
end)
if reserved_type(type) do
Map.put(acc, "#{String.downcase(String.replace(context.module_name, "AWS.", ""))}_#{type}", types)
Expand All @@ -79,7 +77,7 @@ defmodule AWS.CodeGen.Util do
end)
end

defp is_required(is_input, shape, target) do
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
Expand All @@ -95,14 +93,27 @@ defmodule AWS.CodeGen.Util do
"\"#{trimmed_name}\""
end
end
defp is_required(:erlang, _is_input, _shape, target) do
# There is no optional/1 | required/1 type of thing in Erlang.
# Instead we'd use := | => but let's deal with that problem later...
trimmed_name = String.replace(target, ~r/com\.amazonaws\.[^#]+#/, "")
"\"#{trimmed_name}\""
end

def function_argument_type(action) do
def function_argument_type(:elixir, action) do
case Map.get(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.get(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(action) do
case Map.get(action.output, "target") do
Expand All @@ -123,6 +134,25 @@ defmodule AWS.CodeGen.Util do
Enum.join([normal, "{:error, {:unexpected_response, any()}}" | errors], " | ")
end
end
def return_type(:erlang, action) do
case Map.get(action.output, "target") do
"smithy.api#Unit" -> "[]"
type ->
normal = "{ok, #{AWS.CodeGen.Name.to_snake_case(String.replace(type, ~r/com\.amazonaws\.[^#]+#/, ""))}(), tuple()}"
errors =
if is_list(action.errors) do
Enum.map(action.errors,
fn %{"target" => error_type} ->
"{error, #{AWS.CodeGen.Name.to_snake_case(String.replace(error_type, ~r/com\.amazonaws\.[^#]+#/, ""))}(), tuple()}"
_ ->
""
end )
else
[]
end
Enum.join([normal, "{error, any()}" | errors], " |\n ")
end
end

def reserved_type(type) do
if type == "node" || type == "term" do
Expand Down
12 changes: 12 additions & 0 deletions priv/post.erl.eex
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,23 @@

-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 %>

%%====================================================================
%% API
%%====================================================================
<%= for action <- context.actions do %>
<%= action.docstring %>
-spec <%= action.function_name %>(map(), <%= AWS.CodeGen.Util.function_argument_type(context.language, action)%>, list()) ->
<%= 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, []).
Expand Down
2 changes: 1 addition & 1 deletion priv/post.ex.eex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ end) %>
@doc """
<%= action.docstring %>
"""<% end %>
@spec <%= action.function_name %>(map(), <%= AWS.CodeGen.Util.function_argument_type(action)%>, list()) :: <%= AWS.CodeGen.Util.return_type(action)%>
@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 %>
Expand Down

0 comments on commit 92118ae

Please sign in to comment.