diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c5efb6..f5f6e91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,47 +6,12 @@ on: - '*' jobs: - old_otp_version_tests: - runs-on: ubuntu-20.04 - name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} - strategy: - matrix: - include: - - elixir: '1.10' - otp: '22' - - elixir: '1.10' - otp: '23' - - elixir: '1.11' - otp: '22' - - elixir: '1.11' - otp: '23' - - elixir: '1.12' - otp: '22' - - elixir: '1.12' - otp: '23' - - elixir: '1.13' - otp: '22' - - elixir: '1.13' - otp: '23' - - elixir: '1.14' - otp: '23' - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} - - run: mix deps.get - - run: mix test - version_tests: runs-on: ubuntu-latest name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: include: - - elixir: '1.11' - otp: '24' - elixir: '1.12' otp: '24' - elixir: '1.13' @@ -65,6 +30,12 @@ jobs: otp: '25' - elixir: '1.15' otp: '26' + - elixir: '1.16' + otp: '24' + - elixir: '1.16' + otp: '25' + - elixir: '1.16' + otp: '26' steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 @@ -76,7 +47,7 @@ jobs: all_versions_tests: name: "All Versions Tests" - needs: [old_otp_version_tests, version_tests] + needs: version_tests runs-on: ubuntu-latest steps: - run: echo "Elixir tests for many versions of Elixir and OTP have successfully completed." diff --git a/CHANGELOG.md b/CHANGELOG.md index a119688..d230ba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog for v0.x +## v2.0.0 (2024-03-11) + +### Enhancements + + * [Airbrake.Utils] Add `detuple/1` to support CBRelay in parsing Airbrake params before transmitting + * [Airbrake.Utils] Add `destruct/1` to support CBRelay in parsing Airbrake params before transmitting + +### Breaking Change + + * Drop support for Elixir <1.12 + ## v1.0.0 (2023-10-12) ### Enhancements diff --git a/lib/airbrake/utils.ex b/lib/airbrake/utils.ex index a1a51e0..826c3a6 100644 --- a/lib/airbrake/utils.ex +++ b/lib/airbrake/utils.ex @@ -33,4 +33,51 @@ defmodule Airbrake.Utils do do: {k, @filtered_value}, else: {k, filter(v, filtered_attributes)} end + + @doc """ + Turns tuples into lists for JSON serialization of Airbrake payloads + """ + def detuple(%module{} = struct) do + fields = struct |> Map.from_struct() |> detuple() + struct(module, fields) + end + + def detuple(map) when is_map(map) do + Enum.into(map, %{}, fn {k, v} -> {detuple(k), detuple(v)} end) + end + + def detuple(list) when is_list(list) do + Enum.map(list, &detuple/1) + end + + def detuple(tuple) when is_tuple(tuple) do + tuple |> Tuple.to_list() |> detuple() + end + + def detuple(other) do + other + end + + @doc """ + Recursively breaks down structs for JSON serialization of Airbrake payloads + """ + def destruct(%_module{} = struct) do + struct |> Map.from_struct() |> destruct() + end + + def destruct(map) when is_map(map) do + Enum.into(map, %{}, fn {k, v} -> {destruct(k), destruct(v)} end) + end + + def destruct(list) when is_list(list) do + Enum.map(list, &destruct/1) + end + + def destruct(tuple) when is_tuple(tuple) do + tuple |> Tuple.to_list() |> destruct() |> List.to_tuple() + end + + def destruct(other) do + other + end end diff --git a/mix.exs b/mix.exs index 510e68b..9498352 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Airbrake.Mixfile do [ app: :airbrake_client, version: "1.0.0", - elixir: "~> 1.10", + elixir: "~> 1.12", elixirc_paths: elixirc_paths(Mix.env()), package: package(), aliases: aliases(), diff --git a/test/airbrake/utils_test.exs b/test/airbrake/utils_test.exs index 27b74bc..d1597bd 100644 --- a/test/airbrake/utils_test.exs +++ b/test/airbrake/utils_test.exs @@ -4,8 +4,6 @@ defmodule Airbrake.UtilsTest do alias Airbrake.Utils - @moduletag :focus - defmodule Struct do defstruct [:baz, :qux] end @@ -48,4 +46,90 @@ defmodule Airbrake.UtilsTest do } end end + + describe "detuple/1" do + test "converts tuples to lists" do + input = %Struct{baz: {1, 2}, qux: {:ok, "foobar"}} + expected = %Struct{baz: [1, 2], qux: [:ok, "foobar"]} + + assert Utils.detuple(input) == expected + end + + test "returns de-tupled data when input contains deeply nested tuples" do + input = %Struct{baz: [1, 2, {3, 4}], qux: %{foo: %{bar: {9, 9, 9, 9}}}} + expected = %Struct{baz: [1, 2, [3, 4]], qux: %{foo: %{bar: [9, 9, 9, 9]}}} + + assert Utils.detuple(input) == expected + end + + test "returns detupled data when input is a map with tuples" do + input = %{baz: {1, 2}, qux: {:ok, "sucess"}} + expected = %{baz: [1, 2], qux: [:ok, "sucess"]} + + assert Utils.detuple(input) == expected + end + + test "returns detupled data when input is a list with tuples" do + input = ["foo", {:ok, "sucess"}] + expected = ["foo", [:ok, "sucess"]] + + assert Utils.detuple(input) == expected + end + + test "returns a list when input is nested tuples" do + input = {:ok, {:error, "something"}} + expected = [:ok, [:error, "something"]] + + assert Utils.detuple(input) == expected + end + + property "scalars are returned unchanged" do + check all scalar <- one_of([integer(), float(), string(:utf8), atom(:alphanumeric), boolean()]) do + assert Utils.detuple(scalar) == scalar + end + end + end + + describe "destruct/1" do + test "converts structs to maps" do + input = %{a: %Struct{baz: 100, qux: 200}, b: "foo", c: %Struct{baz: 1, qux: 2}} + expected = %{a: %{baz: 100, qux: 200}, b: "foo", c: %{baz: 1, qux: 2}} + + assert Utils.destruct(input) == expected + end + + test "returns de-structed data when input contains deeply nested structs" do + input = %Struct{baz: [1, 2, {3, %Struct{baz: "bar"}}], qux: %{foo: %{bar: %Struct{baz: "bar"}, qux: "foo"}}} + expected = %{baz: [1, 2, {3, %{baz: "bar", qux: nil}}], qux: %{foo: %{bar: %{baz: "bar", qux: nil}, qux: "foo"}}} + + assert Utils.destruct(input) == expected + end + + test "returns de-structed data when input is a list containing a struct" do + input = ["foo", %Struct{baz: 100, qux: 200}] + expected = ["foo", %{baz: 100, qux: 200}] + + assert Utils.destruct(input) == expected + end + + test "returns the de-structed data when input is a tuple containing a struct" do + input = {:ok, %Struct{baz: 100, qux: 200}} + expected = {:ok, %{baz: 100, qux: 200}} + + assert Utils.destruct(input) == expected + end + + test "returns a map of the original data when input contains nested structs" do + input = %Struct{baz: 100, qux: %Struct{baz: 100, qux: 200}} + expected = %{baz: 100, qux: %{baz: 100, qux: 200}} + + assert Utils.destruct(input) == expected + end + + property "scalars are returned unchanged" do + check all scalar <- one_of([integer(), float(), string(:utf8), atom(:alphanumeric), boolean()]) do + assert Utils.destruct(scalar) == scalar + end + end + end end