Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/github/plugin/typed_decoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ defmodule GitHub.Plugin.TypedDecoder do
defp do_decode(value, [type]), do: Enum.map(value, &do_decode(&1, type))

defp do_decode(%{} = value, {module, type}) do
base = if function_exported?(module, :__struct__, 0), do: struct(module), else: %{}
# `function_exported?/3` returns false for a module that has not been loaded yet, so under
# interactive code loading the first response decoded for any given schema would fall back to
# a bare map. (The `module.__fields__/1` call below is what loads the module, meaning every
# subsequent decode returns the struct — an intermittent failure for callers matching on it.)
base =
if Code.ensure_loaded?(module) and function_exported?(module, :__struct__, 0),
do: struct(module),
else: %{}

fields = module.__fields__(type)

for {field_name, field_type} <- fields, reduce: base do
Expand Down
40 changes: 40 additions & 0 deletions test/github/plugin/typed_decoder_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule GitHub.Plugin.TypedDecoderTest do
use ExUnit.Case

alias GitHub.Plugin.TypedDecoder

# Stands in for a type whose module defines `__fields__/1` without a struct.
defmodule StructlessSchema do
def __fields__(:t), do: [name: {:string, :generic}]
end

describe "decode/2" do
test "decodes a struct when the schema module is loaded" do
Code.ensure_loaded!(GitHub.Installation.Token)

assert %GitHub.Installation.Token{token: "ghs_abc"} =
TypedDecoder.decode(%{"token" => "ghs_abc"}, {GitHub.Installation.Token, :t})
end

test "decodes a struct when the schema module has not been loaded yet" do
unload(GitHub.Installation.Token)
refute :erlang.module_loaded(GitHub.Installation.Token)

assert %GitHub.Installation.Token{token: "ghs_abc"} =
TypedDecoder.decode(%{"token" => "ghs_abc"}, {GitHub.Installation.Token, :t})
end

test "decodes a bare map when the module has no struct" do
assert TypedDecoder.decode(%{"name" => "test"}, {StructlessSchema, :t}) == %{name: "test"}
end
end

# Returns the module to the state it has on a cold VM, before anything has
# referenced it. Reloaded afterwards so the rest of the suite is unaffected.
defp unload(module) do
on_exit(fn -> Code.ensure_loaded(module) end)

:code.purge(module)
:code.delete(module)
end
end