From aae1927b251cc043a07faa222f7484fe9498bed0 Mon Sep 17 00:00:00 2001 From: Keith Brings Date: Mon, 12 Apr 2021 16:38:56 +0700 Subject: [PATCH 1/4] Merging Template support from solace club fork. --- .../transactional_templates/template.ex | 37 ++++++++ .../template/version.ex | 45 ++++++++++ .../template/versions.ex | 72 +++++++++++++++ .../transactional_templates/templates.ex | 71 +++++++++++++++ mix.lock | 30 +++---- test/transactional_templates_test.exs | 87 +++++++++++++++++++ 6 files changed, 327 insertions(+), 15 deletions(-) create mode 100644 lib/sendgrid/transactional_templates/template.ex create mode 100644 lib/sendgrid/transactional_templates/template/version.ex create mode 100644 lib/sendgrid/transactional_templates/template/versions.ex create mode 100644 lib/sendgrid/transactional_templates/templates.ex create mode 100644 test/transactional_templates_test.exs diff --git a/lib/sendgrid/transactional_templates/template.ex b/lib/sendgrid/transactional_templates/template.ex new file mode 100644 index 0000000..5db37cf --- /dev/null +++ b/lib/sendgrid/transactional_templates/template.ex @@ -0,0 +1,37 @@ +defmodule SendGrid.Template do + @moduledoc """ + Module to interact with transaction email templates. + """ + alias __MODULE__ + + @derive Jason.Encoder + defstruct [ + id: nil, + name: nil, + versions: nil + ] + + @type t :: %Template{ + id: String.t, + name: String.t, + versions: [Versions.t] + } + + @spec new(Map.t, :json) :: Template.t + def new(json, :json) do + # versions. + versions = case json["versions"] do + nil -> [] + versions when is_list(versions) -> + for version <- versions do + SendGrid.Template.Version.new(version, :json) + end + end + + %Template{ + id: json["id"], + name: json["name"], + versions: versions + } + end +end diff --git a/lib/sendgrid/transactional_templates/template/version.ex b/lib/sendgrid/transactional_templates/template/version.ex new file mode 100644 index 0000000..885a8df --- /dev/null +++ b/lib/sendgrid/transactional_templates/template/version.ex @@ -0,0 +1,45 @@ +defmodule SendGrid.Template.Version do + @moduledoc """ + Module to interact with transaction email template versions. + """ + alias __MODULE__ + + @derive Jason.Encoder + defstruct [ + id: nil, + template_id: nil, + active: nil, + name: nil, + html_content: "", + plain_content: "", + subject: "", + updated_at: nil + ] + + @type t :: %Version{ + id: String.t, + template_id: String.t, + active: nil, + name: String.t, + html_content: String.t, + plain_content: String.t, + subject: String.t, + updated_at: String.t # Should be converted to unix epoch or Timex + } + + @spec new(Map.t, :json) :: Version.t + def new(json, :json) do + %Version{ + id: json["id"], + template_id: json["template_id"], + active: json["active"] == 1, + name: json["name"], + subject: json["subject"], + updated_at: json["updated_at"], + + # content only returned when specifically fetching entry by id. Will be null otherwise. + html_content: json["html_content"], + plain_content: json["plain_content"] + } + end +end diff --git a/lib/sendgrid/transactional_templates/template/versions.ex b/lib/sendgrid/transactional_templates/template/versions.ex new file mode 100644 index 0000000..ea13290 --- /dev/null +++ b/lib/sendgrid/transactional_templates/template/versions.ex @@ -0,0 +1,72 @@ +defmodule SendGrid.Template.Versions do + alias SendGrid.Template.Version + + @success_codes [200,201,202,203,204] + + @spec activate(Version.t) :: Version.t | {:error, [String.t]} | {:error, String.t} + def activate(%Version{} = version) do + case SendGrid.post(base_url(version.template_id, version.id) <> "/activate", version, []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + Version.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"]} + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec get(String.t, String.t) :: Version.t | {:error, [String.t]} | {:error, String.t} + def get(template, version) do + case SendGrid.get(base_url(template,version), []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + Version.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec update(Version.t) :: Version.t | {:error, [String.t]} | {:error, String.t} + def update(%Version{} = version) do + case SendGrid.patch(base_url(version.template_id, version.id), version, []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + response = Version.new(response.body, :json) + put_in(response, [Access.key(:template_id)], version.template_id) + { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec create(Version.t) :: Version.t | {:error, [String.t]} | {:error, String.t} + def create(%Version{} = version) do + case SendGrid.post(base_url(version.template_id), version, []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + response = Version.new(response.body, :json) + if response.id != nil do + get(version.template_id, response.id) + else + {:error, :post_create_fetch_failure} + end + + { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"]} + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec delete(Version.t) :: :ok | {:error, [String.t]} | {:error, String.t} + def delete(%Version{} = version) do + case SendGrid.delete(base_url(version.template_id, version.id), []) do + { :ok, %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + :ok + { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + defp base_url(template) do + "/v3/templates/#{template}/versions" + end + + defp base_url(template, version) do + "/v3/templates/#{template}/versions/#{version}" + end + +end diff --git a/lib/sendgrid/transactional_templates/templates.ex b/lib/sendgrid/transactional_templates/templates.ex new file mode 100644 index 0000000..aff8aee --- /dev/null +++ b/lib/sendgrid/transactional_templates/templates.ex @@ -0,0 +1,71 @@ +defmodule SendGrid.Templates do + alias SendGrid.Template + @base_api_url "/v3/templates" + + @success_codes [200,201,202,203,204] + + @spec get(String.t) :: Template.t | {:error, [String.t]} | {:error, String.t} + def get(identifier) do + case SendGrid.get(@base_api_url <> "/#{identifier}", []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec update(Template.t) :: Template.t | {:error, [String.t]} | {:error, String.t} + def update(%Template{} = template) do + case SendGrid.patch(@base_api_url <> "/#{template.id}", template, []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec create(Template.t) :: Template.t | {:error, [String.t]} | {:error, String.t} + def create(%Template{} = template) do + case SendGrid.post(@base_api_url, template, []) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body }} -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec delete(Template.t) :: :ok | {:error, [String.t]} | {:error, String.t} + def delete(%Template{} = template) do + delete(template.id) + end + + @spec delete(String.t) :: :ok | {:error, [String.t]} | {:error, String.t} + def delete(identifier) when is_bitstring(identifier) do + case SendGrid.delete(@base_api_url <> "/#{identifier}", []) do + { :ok, %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + :ok + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> + { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec list() :: [SendGrid.Template.t] | {:error, [String.t]} | {:error, String.t} + def list() do + fetch = SendGrid.get(@base_api_url, []) + case fetch do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + for template <- response.body["templates"] do + SendGrid.Template.new(template, :json) + end + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> + { :error, "Unable to communicate with SendGrid API." } + end + end + +end diff --git a/mix.lock b/mix.lock index 8fc0c58..8b026ab 100644 --- a/mix.lock +++ b/mix.lock @@ -1,27 +1,27 @@ %{ "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, - "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, - "erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "4bba10c6f267a0dd127d687d1295f6a11af6a7f160cc0e261c46f1962a98d7d8"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, + "erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm", "f9388f7d1a668bee6ebddc040422ed6340af74aced153e492330da4c39516d92"}, + "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "dc87f778d8260da0189a622f62790f6202af72f2f3dee6e78d91a18dd2fcd137"}, "floki": {:hex, :floki, "0.17.2", "81b3a39d85f5cae39c8da16236ce152f7f8f50faf84b480ba53351d7e96ca6ca", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, optional: false]}]}, "hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, "httpoison": {:hex, :httpoison, "0.11.0", "b9240a9c44fc46fcd8618d17898859ba09a3c1b47210b74316c0ffef10735e76", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, + "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm", "33dd09e615daab5668c15cc3a33829892728fdbed910ab0c0a0edb06b45fc54d"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], []}, - "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, - "phoenix": {:hex, :phoenix, "1.2.4", "4172479b5e21806a5e4175b54820c239e0d4effb0b07912e631aa31213a05bae", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.4 or ~> 1.3.3 or ~> 1.2.4 or ~> 1.1.8 or ~> 1.0.5", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, - "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], []}, - "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, - "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"}, + "phoenix": {:hex, :phoenix, "1.2.4", "4172479b5e21806a5e4175b54820c239e0d4effb0b07912e631aa31213a05bae", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.4 or ~> 1.3.3 or ~> 1.2.4 or ~> 1.1.8 or ~> 1.0.5", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "e75dcda4cce7acdb72a10d4780914d288b5c728f6ea58fd85cf41671cd56c35b"}, + "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f090e3a75de4efd72ae2b9b016909fe673512ef04e6b72d7bf039031a64d4a52"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm", "6f9193364c5de86b85e8d3a80294a134aecf6c5618adcbae668608749e00a7f7"}, + "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "141058cca1fa800128391ece7f442f71a7b42a7411e6eaa56dc8f85283c8dde7"}, + "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], [], "hexpm", "519bc209e4433961284174c497c8524c001e285b79bdf80212b47a1f898084cc"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}, - "tesla": {:hex, :tesla, "1.2.0", "9e2469c1bcdb0cc8fe5fd3e9208432f3fee8e439a44f639d8ce72bcccd6f3566", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "tesla": {:hex, :tesla, "1.2.0", "9e2469c1bcdb0cc8fe5fd3e9208432f3fee8e439a44f639d8ce72bcccd6f3566", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "9d6f9957d90ff2fdd370d62a3704b101dc0d11be73994b3794a43c37f01c1489"}, } diff --git a/test/transactional_templates_test.exs b/test/transactional_templates_test.exs new file mode 100644 index 0000000..191cd3d --- /dev/null +++ b/test/transactional_templates_test.exs @@ -0,0 +1,87 @@ +defmodule SendGrid.TransactionalTemplates.Test do + @moduledoc """ + Module to test Transactional Template CRUD. Not this module requires that api key has read/write/update/delete permissions for templates and versions. + """ + use ExUnit.Case, async: true + doctest SendGrid.Templates, import: true + doctest SendGrid.Template, import: true + doctest SendGrid.Template.Version, import: true + require Logger + alias SendGrid.Templates + alias SendGrid.Template + alias SendGrid.Template.Versions + alias SendGrid.Template.Version + + @tag :templates + test "fetch templates" do + actual = Templates.list() + assert is_list(actual) + end + + @tag :templates + test "template crud" do + # Note Better fixture management needed to avoid orphaned elements. + # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. + + test_run = :os.system_time(:millisecond) + name = "TestTemplate#{test_run}" + updated_name = "UpdatedTemplateName#{test_run}" + + # Create Template + new_template = Templates.create(%Template{name: name}) + assert new_template.id != nil + + try do + # Update Template + _updated_template = Templates.update(%Template{new_template| name: updated_name}) + + # Read Template + read_template = Templates.get(new_template.id) + assert read_template.name == updated_name + after + # Delete Template + Templates.delete(new_template) + {:error, _} = Templates.get(new_template.id) + end + end + + @tag :templates + test "template.version crud" do + # Note Better fixture management needed to avoid orphaned elements. + # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. + + test_run = :os.system_time(:millisecond) + name = "TestTemplate#{test_run}" + test_version_name = "TestVersion#{test_run}" + updated_test_version = "TestVersionUpdated#{test_run}" + + # Create Template + new_template = Templates.create(%Template{name: name}) + template_id = new_template.id + assert template_id != nil + + try do + # Create Version + new_version = Versions.create(%Version{name: test_version_name, template_id: template_id, subject: "Hello World", html_content: "Hello", plain_content: "Hello txt"}) + assert new_version.id != nil + + # Update Version + updated_version = Versions.update(%Version{new_version| name: updated_test_version}) + + # Get Version + read_version = Versions.get(updated_version.template_id, updated_version.id) + assert read_version.name == updated_test_version + + # Delete Version & Confirm + delete_version = Versions.delete(read_version) + assert delete_version == :ok + {:error, _details} = Versions.get(new_version.template_id, new_version.id) + after + Templates.delete(new_template) + end + + {:error, _} = Templates.get(new_template.id) + end + + +end From 5a08f8db82fbc5f9e9c896b281e50339c2c0278d Mon Sep 17 00:00:00 2001 From: Keith Brings Date: Sun, 18 Apr 2021 16:17:32 +0700 Subject: [PATCH 2/4] - Adding additional API fields - Stripping read only fields and unset fields on Version json encoder. - Adding Dynamic + Legacy Tests --- lib/sendgrid/email.ex | 5 +- lib/sendgrid/templates/metadata.ex | 38 ++++ .../template.ex | 22 +++ lib/sendgrid/templates/template/version.ex | 86 +++++++++ .../template/versions.ex | 30 ++-- lib/sendgrid/templates/templates.ex | 156 ++++++++++++++++ .../template/version.ex | 45 ----- .../transactional_templates/templates.ex | 71 -------- test/templates_test.exs | 166 ++++++++++++++++++ test/transactional_templates_test.exs | 87 --------- 10 files changed, 486 insertions(+), 220 deletions(-) create mode 100644 lib/sendgrid/templates/metadata.ex rename lib/sendgrid/{transactional_templates => templates}/template.ex (50%) create mode 100644 lib/sendgrid/templates/template/version.ex rename lib/sendgrid/{transactional_templates => templates}/template/versions.ex (68%) create mode 100644 lib/sendgrid/templates/templates.ex delete mode 100644 lib/sendgrid/transactional_templates/template/version.ex delete mode 100644 lib/sendgrid/transactional_templates/templates.ex create mode 100644 test/templates_test.exs delete mode 100644 test/transactional_templates_test.exs diff --git a/lib/sendgrid/email.ex b/lib/sendgrid/email.ex index 954cb53..1a56c7c 100644 --- a/lib/sendgrid/email.ex +++ b/lib/sendgrid/email.ex @@ -33,7 +33,7 @@ defmodule SendGrid.Email do You can provide a Unix timestamp to have an email delivered in the future. - send_at(email, 1409348513) + put_send_at(email, 1409348513) ## Phoenix Views @@ -359,7 +359,8 @@ defmodule SendGrid.Email do Email.put_template(%Email{}, "the_template_id") """ - @spec put_template(t, String.t()) :: t + @spec put_template(t, String.t() | SendGrid.Template) :: t + def put_template(%Email{} = email, template = %SendGrid.Template{}), do: put_template(email, template.id) def put_template(%Email{} = email, template_id) do %Email{email | template_id: template_id} end diff --git a/lib/sendgrid/templates/metadata.ex b/lib/sendgrid/templates/metadata.ex new file mode 100644 index 0000000..aa94d83 --- /dev/null +++ b/lib/sendgrid/templates/metadata.ex @@ -0,0 +1,38 @@ +defmodule SendGrid.MetaData do + @moduledoc """ + Pagination Meta Details, + """ + + defstruct [ + self: nil, + next: nil, + count: nil, + options: nil, + ] + + @type t :: %SendGrid.MetaData{ + self: String.t | nil, + next: String.t | nil, + count: integer, + options: SendGrid.query(), + } + + @spec new(Map.t, SendGrid.query(), :json) :: SendGrid.MetaData.t | {:error, [String.t]} | {:error, String.t} + def new(json, options, :json) do + %__MODULE__{ + count: json["count"], + self: extract_page_token(json["self"]), + next: extract_page_token(json["next"]), + options: options + } + end + + defp extract_page_token(url) when is_bitstring(url) do + case Regex.run(~r/.*page_token=([^&]+)/, url) do + [_, m] -> m + _ -> nil + end + end + defp extract_page_token(_), do: nil + +end \ No newline at end of file diff --git a/lib/sendgrid/transactional_templates/template.ex b/lib/sendgrid/templates/template.ex similarity index 50% rename from lib/sendgrid/transactional_templates/template.ex rename to lib/sendgrid/templates/template.ex index 5db37cf..b4e5124 100644 --- a/lib/sendgrid/transactional_templates/template.ex +++ b/lib/sendgrid/templates/template.ex @@ -4,16 +4,25 @@ defmodule SendGrid.Template do """ alias __MODULE__ + @generations %{ + "dynamic" => :dynamic, + "legacy" => :legacy, + } + @derive Jason.Encoder defstruct [ id: nil, name: nil, + updated_at: nil, + generation: nil, versions: nil ] @type t :: %Template{ id: String.t, name: String.t, + updated_at: DateTime.t | nil, + generation: :dynamic | :legacy | String.t, versions: [Versions.t] } @@ -31,7 +40,20 @@ defmodule SendGrid.Template do %Template{ id: json["id"], name: json["name"], + updated_at: reformat_sendgrid_date(json["updated_at"]), + generation: @generations[json["generation"]] || json["generation"], versions: versions } end + + defp reformat_sendgrid_date(date = %DateTime{}), do: date + defp reformat_sendgrid_date(date) when is_bitstring(date) do + [a,b] = String.split(date) + case DateTime.from_iso8601("#{a} #{b}.0Z") do + {:ok, d, _} -> d + _ -> nil + end + end + defp reformat_sendgrid_date(_), do: nil + end diff --git a/lib/sendgrid/templates/template/version.ex b/lib/sendgrid/templates/template/version.ex new file mode 100644 index 0000000..07aade6 --- /dev/null +++ b/lib/sendgrid/templates/template/version.ex @@ -0,0 +1,86 @@ +defmodule SendGrid.Template.Version do + @moduledoc """ + Module to interact with transaction email template versions. + """ + alias __MODULE__ + + @derive Jason.Encoder + defstruct [ + id: nil, + template_id: nil, + updated_at: nil, + thumbnail_url: nil, + warnings: nil, + active: nil, + name: nil, + + html_content: "", + plain_content: "", + generate_plain_content: nil, + subject: "", + editor: nil, + test_data: nil, + ] + + @type t :: %Version{ + id: String.t, + template_id: String.t, + updated_at: DateTime.t, + thumbnail_url: String.t, + warnings: list | nil, + active: integer | nil, + name: String.t, + + html_content: String.t, + plain_content: String.t, + generate_plain_content: boolean | nil, + subject: String.t, + editor: String.t | nil, + test_data: Map.t | nil, + } + + @spec new(Map.t, :json) :: Version.t + def new(json, :json) do + %Version{ + id: json["id"], + template_id: json["template_id"], + updated_at: reformat_sendgrid_date(json["updated_at"]), + thumbnail_url: json["thumbnail_url"], + warnings: json["warnings"], + active: json["active"], + name: json["name"], + # content only returned when specifically fetching entry by id. Will be null otherwise. + html_content: json["html_content"], + plain_content: json["plain_content"], + generate_plain_content: json["generate_plain_content"], + subject: json["subject"], + editor: json["editor"], + test_data: json["test_data"] + } + end + + defp reformat_sendgrid_date(date = %DateTime{}), do: date + defp reformat_sendgrid_date(date) when is_bitstring(date) do + [a,b] = String.split(date) + case DateTime.from_iso8601("#{a} #{b}.0Z") do + {:ok, d, _} -> d + _ -> nil + end + end + defp reformat_sendgrid_date(_), do: nil + + + + defimpl Jason.Encoder do + def encode(%SendGrid.Template.Version{} = version, opts) do + raw = version + |> Map.from_struct() + |> Map.drop([:updated_at, :editor, :thumbnail_url]) + |> Enum.filter(fn({_k,v}) -> v != nil end) + |> Map.new() + Jason.Encode.map(raw, opts) + end + end + + +end diff --git a/lib/sendgrid/transactional_templates/template/versions.ex b/lib/sendgrid/templates/template/versions.ex similarity index 68% rename from lib/sendgrid/transactional_templates/template/versions.ex rename to lib/sendgrid/templates/template/versions.ex index ea13290..aaaa9dc 100644 --- a/lib/sendgrid/transactional_templates/template/versions.ex +++ b/lib/sendgrid/templates/template/versions.ex @@ -3,9 +3,9 @@ defmodule SendGrid.Template.Versions do @success_codes [200,201,202,203,204] - @spec activate(Version.t) :: Version.t | {:error, [String.t]} | {:error, String.t} - def activate(%Version{} = version) do - case SendGrid.post(base_url(version.template_id, version.id) <> "/activate", version, []) do + @spec activate(Version.t, SendGrid.query()) :: Version.t | {:error, [String.t]} | {:error, String.t} + def activate(%Version{} = version, options \\ []) do + case SendGrid.post(base_url(version.template_id, version.id) <> "/activate", version, options) do { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> Version.new(response.body, :json) { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"]} @@ -13,9 +13,9 @@ defmodule SendGrid.Template.Versions do end end - @spec get(String.t, String.t) :: Version.t | {:error, [String.t]} | {:error, String.t} - def get(template, version) do - case SendGrid.get(base_url(template,version), []) do + @spec get(String.t, String.t, SendGrid.query()) :: Version.t | {:error, [String.t]} | {:error, String.t} + def get(template, version, options \\ []) do + case SendGrid.get(base_url(template,version), options) do { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> Version.new(response.body, :json) { :ok, %SendGrid.Response{ body: body } } -> @@ -24,9 +24,9 @@ defmodule SendGrid.Template.Versions do end end - @spec update(Version.t) :: Version.t | {:error, [String.t]} | {:error, String.t} - def update(%Version{} = version) do - case SendGrid.patch(base_url(version.template_id, version.id), version, []) do + @spec update(Version.t, SendGrid.query()) :: Version.t | {:error, [String.t]} | {:error, String.t} + def update(%Version{} = version, options \\ []) do + case SendGrid.patch(base_url(version.template_id, version.id), version, options) do { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> response = Version.new(response.body, :json) put_in(response, [Access.key(:template_id)], version.template_id) @@ -35,9 +35,9 @@ defmodule SendGrid.Template.Versions do end end - @spec create(Version.t) :: Version.t | {:error, [String.t]} | {:error, String.t} - def create(%Version{} = version) do - case SendGrid.post(base_url(version.template_id), version, []) do + @spec create(Version.t, SnedGrid.query()) :: Version.t | {:error, [String.t]} | {:error, String.t} + def create(%Version{} = version, options \\ []) do + case SendGrid.post(base_url(version.template_id), version, options) do { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> response = Version.new(response.body, :json) if response.id != nil do @@ -51,9 +51,9 @@ defmodule SendGrid.Template.Versions do end end - @spec delete(Version.t) :: :ok | {:error, [String.t]} | {:error, String.t} - def delete(%Version{} = version) do - case SendGrid.delete(base_url(version.template_id, version.id), []) do + @spec delete(Version.t, SendGrid.query()) :: :ok | {:error, [String.t]} | {:error, String.t} + def delete(%Version{} = version, options \\ []) do + case SendGrid.delete(base_url(version.template_id, version.id), options) do { :ok, %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> :ok { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"] } diff --git a/lib/sendgrid/templates/templates.ex b/lib/sendgrid/templates/templates.ex new file mode 100644 index 0000000..ea07b0b --- /dev/null +++ b/lib/sendgrid/templates/templates.ex @@ -0,0 +1,156 @@ +defmodule SendGrid.Templates do + alias SendGrid.Template + @base_api_url "/v3/templates" + @valid_generations [:legacy, :dynamic] + @success_codes [200,201,202,203,204] + + #---------------------------------------- + # Result Set + #---------------------------------------- + defstruct [ + templates: [], + metadata: nil, + ] + + @type t :: %SendGrid.Templates{ + templates: [SendGrid.Template.t], + metadata: SendGrid.MetaData.t | nil, + } + + @spec new(SendGrid.Response.t, SendGrid.query()) :: Templates.t | {:error, [String.t]} | {:error, String.t} + def new(%SendGrid.Response{body: %{"_metadata" => metadata, "result" => result}}, options) do + %__MODULE__{ + templates: Enum.map(result, &(SendGrid.Template.new(&1, :json))), + metadata: SendGrid.MetaData.new(metadata, options, :json) + } + end + def new(%SendGrid.Response{body: %{"templates" => templates}}, options) do + %__MODULE__{ + templates: Enum.map(templates, &(SendGrid.Template.new(&1, :json))), + metadata: SendGrid.MetaData.new(%{}, options, :json) + } + end + def new(_), do: {:error, "#{__MODULE__} Unsupported Initializer"} + + #---------------------------------------- + # Pagination + #---------------------------------------- + @spec get(SendGrid.Templates.t, SendGrid.query()) :: Templates.t | {:error, [String.t]} | {:error, String.t} + def next(self, options \\ []) + def next(%SendGrid.Templates{metadata: %SendGrid.MetaData{next: nil}}, options) do + nil + end + def next(%SendGrid.Templates{} = self, options) do + # Note only api_key may be changed when calling next + options = cond do + api_key = options[:api_key] -> Keyword.put(self.metadata.options || [], :api_key, api_key) + :else -> self.metadata.options || [] + end + options = cond do + query = options[:query] -> + query = Keyword.put(query, :page_token, self.metadata.next) + Keyword.put(options, :query, query) + :else -> + Keyword.put(options, :query, [page_token: self.metadata.next]) + end + fetch = SendGrid.get(@base_api_url, options) + case fetch do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + __MODULE__.new(response, options) + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> + { :error, "Unable to communicate with SendGrid API." } + end + end + + #---------------------------------------- + # CRUD + #---------------------------------------- + @spec get(String.t, SendGrid.query()) :: Template.t | {:error, [String.t]} | {:error, String.t} + def get(identifier, options \\ []) do + options = patch_options(options) + case SendGrid.get(@base_api_url <> "/#{identifier}", options) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec update(Template.t, SendGrid.query()) :: Template.t | {:error, [String.t]} | {:error, String.t} + def update(%Template{} = template, options \\ []) do + options = patch_options(options) + case SendGrid.patch(@base_api_url <> "/#{template.id}", template, options) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec create(Template.t, SendGrid.query()) :: Template.t | {:error, [String.t]} | {:error, String.t} + def create(%Template{} = template, options \\ []) do + options = patch_options(options) + case SendGrid.post(@base_api_url, template, options) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body }} -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec delete(String.t | Template.t, SendGrid.query()) :: :ok | {:error, [String.t]} | {:error, String.t} + def delete(template, options \\ []) + def delete(%Template{} = template, options) do + delete(template.id, options) + end + def delete(identifier, options) when is_bitstring(identifier) do + options = patch_options(options) + case SendGrid.delete(@base_api_url <> "/#{identifier}", options) do + { :ok, %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + :ok + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> + { :error, "Unable to communicate with SendGrid API." } + end + end + + @spec list(SendGrid.query()) :: [SendGrid.Template.t] | {:error, [String.t]} | {:error, String.t} + def list(options \\ []) do + options = patch_options(options) + fetch = SendGrid.get(@base_api_url, options) + case fetch do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + __MODULE__.new(response, options) + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> + { :error, "Unable to communicate with SendGrid API." } + end + end + + #---------------------------------------- + # Support + #---------------------------------------- + @doc """ + Param injector does not handle list data, overriding here to allow user to pass in array or desired generations. + """ + @spec patch_options(SendGrid.query()) :: SendGrid.query() + def patch_options(options \\ []) do + case options[:query][:generations] do + v when is_list(v) -> + generations = v + |> Enum.map(&("#{&1}")) + |> Enum.join(",") + put_in(options, [:query, :generations], generations) + v when is_atom(v) -> options + v when is_bitstring(v) -> options + _else -> options + end + end + +end diff --git a/lib/sendgrid/transactional_templates/template/version.ex b/lib/sendgrid/transactional_templates/template/version.ex deleted file mode 100644 index 885a8df..0000000 --- a/lib/sendgrid/transactional_templates/template/version.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule SendGrid.Template.Version do - @moduledoc """ - Module to interact with transaction email template versions. - """ - alias __MODULE__ - - @derive Jason.Encoder - defstruct [ - id: nil, - template_id: nil, - active: nil, - name: nil, - html_content: "", - plain_content: "", - subject: "", - updated_at: nil - ] - - @type t :: %Version{ - id: String.t, - template_id: String.t, - active: nil, - name: String.t, - html_content: String.t, - plain_content: String.t, - subject: String.t, - updated_at: String.t # Should be converted to unix epoch or Timex - } - - @spec new(Map.t, :json) :: Version.t - def new(json, :json) do - %Version{ - id: json["id"], - template_id: json["template_id"], - active: json["active"] == 1, - name: json["name"], - subject: json["subject"], - updated_at: json["updated_at"], - - # content only returned when specifically fetching entry by id. Will be null otherwise. - html_content: json["html_content"], - plain_content: json["plain_content"] - } - end -end diff --git a/lib/sendgrid/transactional_templates/templates.ex b/lib/sendgrid/transactional_templates/templates.ex deleted file mode 100644 index aff8aee..0000000 --- a/lib/sendgrid/transactional_templates/templates.ex +++ /dev/null @@ -1,71 +0,0 @@ -defmodule SendGrid.Templates do - alias SendGrid.Template - @base_api_url "/v3/templates" - - @success_codes [200,201,202,203,204] - - @spec get(String.t) :: Template.t | {:error, [String.t]} | {:error, String.t} - def get(identifier) do - case SendGrid.get(@base_api_url <> "/#{identifier}", []) do - { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> - SendGrid.Template.new(response.body, :json) - { :ok, %SendGrid.Response{ body: body } } -> { :error, body["errors"] || body["error"] } - _ -> { :error, "Unable to communicate with SendGrid API." } - end - end - - @spec update(Template.t) :: Template.t | {:error, [String.t]} | {:error, String.t} - def update(%Template{} = template) do - case SendGrid.patch(@base_api_url <> "/#{template.id}", template, []) do - { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> - SendGrid.Template.new(response.body, :json) - { :ok, %SendGrid.Response{ body: body } } -> - { :error, body["errors"] || body["error"] } - _ -> { :error, "Unable to communicate with SendGrid API." } - end - end - - @spec create(Template.t) :: Template.t | {:error, [String.t]} | {:error, String.t} - def create(%Template{} = template) do - case SendGrid.post(@base_api_url, template, []) do - { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> - SendGrid.Template.new(response.body, :json) - { :ok, %SendGrid.Response{ body: body }} -> - { :error, body["errors"] || body["error"] } - _ -> { :error, "Unable to communicate with SendGrid API." } - end - end - - @spec delete(Template.t) :: :ok | {:error, [String.t]} | {:error, String.t} - def delete(%Template{} = template) do - delete(template.id) - end - - @spec delete(String.t) :: :ok | {:error, [String.t]} | {:error, String.t} - def delete(identifier) when is_bitstring(identifier) do - case SendGrid.delete(@base_api_url <> "/#{identifier}", []) do - { :ok, %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> - :ok - { :ok, %SendGrid.Response{ body: body } } -> - { :error, body["errors"] || body["error"] } - _ -> - { :error, "Unable to communicate with SendGrid API." } - end - end - - @spec list() :: [SendGrid.Template.t] | {:error, [String.t]} | {:error, String.t} - def list() do - fetch = SendGrid.get(@base_api_url, []) - case fetch do - { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> - for template <- response.body["templates"] do - SendGrid.Template.new(template, :json) - end - { :ok, %SendGrid.Response{ body: body } } -> - { :error, body["errors"] || body["error"] } - _ -> - { :error, "Unable to communicate with SendGrid API." } - end - end - -end diff --git a/test/templates_test.exs b/test/templates_test.exs new file mode 100644 index 0000000..04fe05c --- /dev/null +++ b/test/templates_test.exs @@ -0,0 +1,166 @@ +defmodule SendGrid.Templates.Test do + @moduledoc """ + Module to test Transactional Template CRUD. Not this module requires that api key has read/write/update/delete permissions for templates and versions. + """ + use ExUnit.Case, async: true + doctest SendGrid.Templates, import: true + doctest SendGrid.Template, import: true + doctest SendGrid.Template.Version, import: true + require Logger + alias SendGrid.Templates + alias SendGrid.Template + alias SendGrid.Template.Versions + alias SendGrid.Template.Version + + @tag :templates + test "generations query param patch" do + options = [query: [generations: [:legacy,:dynamic]]] + sut = Templates.patch_options(options) + assert sut == [query: [generations: "legacy,dynamic"]] + end + + @tag :templates + test "fetch templates" do + test_run = :os.system_time(:millisecond) + fixture_a = Templates.create(%Template{name: "TestTemplate#{test_run}a"}) + fixture_b = Templates.create(%Template{name: "TestTemplate#{test_run}b"}) + try do + actual = Templates.list(query: [page_size: 1]) + assert %Templates{} = actual + assert length(actual.templates) == 1 + assert is_bitstring(actual.metadata.self) + assert actual.metadata.count > 1 + after + Templates.delete(fixture_a) + Templates.delete(fixture_b) + end + end + + @tag :templates + test "paginated fetch templates" do + test_run = :os.system_time(:millisecond) + fixture_a = Templates.create(%Template{name: "TestTemplate#{test_run}a"}) + fixture_b = Templates.create(%Template{name: "TestTemplate#{test_run}b"}) + try do + actual = Templates.list(query: [page_size: 1]) + assert %Templates{} = actual + assert length(actual.templates) == 1 + assert is_bitstring(actual.metadata.self) + assert actual.metadata.count > 1 + + next = Templates.next(actual) + assert %Templates{} = actual + assert length(next.templates) == 1 + assert next.metadata.self != actual.metadata.self + assert actual.metadata.count > 1 + after + Templates.delete(fixture_a) + Templates.delete(fixture_b) + end + end + + + @tag :templates + test "template crud" do + # Note Better fixture management needed to avoid orphaned elements. + # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. + + test_run = :os.system_time(:millisecond) + name = "TestTemplate#{test_run}" + updated_name = "UpdatedTemplateName#{test_run}" + + # Create Template + new_template = Templates.create(%Template{name: name}) + assert new_template.id != nil + + try do + # Update Template + _updated_template = Templates.update(%Template{new_template| name: updated_name}) + + # Read Template + read_template = Templates.get(new_template.id) + assert read_template.name == updated_name + after + # Delete Template + Templates.delete(new_template) + end + {:error, _} = Templates.get(new_template.id) + end + + @tag :templates + test "template.version crud (legacy)" do + # Note Better fixture management needed to avoid orphaned elements. + # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. + + test_run = :os.system_time(:millisecond) + name = "TestTemplate#{test_run}" + test_version_name = "TestVersion#{test_run}" + updated_test_version = "TestVersionUpdated#{test_run}" + + # Create Template + new_template = Templates.create(%Template{name: name, generation: :legacy}) + template_id = new_template.id + assert template_id != nil + + try do + # Create Version + new_version = Versions.create(%Version{name: test_version_name, template_id: template_id, subject: "Hello World", html_content: "Hello", plain_content: "Hello txt"}) + assert new_version.id != nil + + # Update Version + updated_version = Versions.update(%Version{new_version| name: updated_test_version, editor: nil}) + # Get Version + read_version = Versions.get(updated_version.template_id, updated_version.id) + assert read_version.name == updated_test_version + + # Delete Version & Confirm + delete_version = Versions.delete(read_version) + assert delete_version == :ok + {:error, _details} = Versions.get(new_version.template_id, new_version.id) + after + Templates.delete(new_template) + end + + {:error, _} = Templates.get(new_template.id) + end + + + @tag :templates + test "template.version crud (dynamic)" do + # Note Better fixture management needed to avoid orphaned elements. + # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. + + test_run = :os.system_time(:millisecond) + name = "TestTemplate#{test_run}" + test_version_name = "TestVersion#{test_run}" + updated_test_version = "TestVersionUpdated#{test_run}" + + # Create Template + new_template = Templates.create(%Template{name: name, generation: :dynamic}) + template_id = new_template.id + assert template_id != nil + + try do + # Create Version + new_version = Versions.create(%Version{name: test_version_name, template_id: template_id, subject: "Hello World", html_content: "Hello", plain_content: "Hello txt"}) + assert new_version.id != nil + + # Update Version + updated_version = Versions.update(%Version{new_version| name: updated_test_version, editor: nil}) + # Get Version + read_version = Versions.get(updated_version.template_id, updated_version.id) + assert read_version.name == updated_test_version + + # Delete Version & Confirm + delete_version = Versions.delete(read_version) + assert delete_version == :ok + {:error, _details} = Versions.get(new_version.template_id, new_version.id) + after + Templates.delete(new_template) + end + + {:error, _} = Templates.get(new_template.id) + end + + +end diff --git a/test/transactional_templates_test.exs b/test/transactional_templates_test.exs deleted file mode 100644 index 191cd3d..0000000 --- a/test/transactional_templates_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -defmodule SendGrid.TransactionalTemplates.Test do - @moduledoc """ - Module to test Transactional Template CRUD. Not this module requires that api key has read/write/update/delete permissions for templates and versions. - """ - use ExUnit.Case, async: true - doctest SendGrid.Templates, import: true - doctest SendGrid.Template, import: true - doctest SendGrid.Template.Version, import: true - require Logger - alias SendGrid.Templates - alias SendGrid.Template - alias SendGrid.Template.Versions - alias SendGrid.Template.Version - - @tag :templates - test "fetch templates" do - actual = Templates.list() - assert is_list(actual) - end - - @tag :templates - test "template crud" do - # Note Better fixture management needed to avoid orphaned elements. - # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. - - test_run = :os.system_time(:millisecond) - name = "TestTemplate#{test_run}" - updated_name = "UpdatedTemplateName#{test_run}" - - # Create Template - new_template = Templates.create(%Template{name: name}) - assert new_template.id != nil - - try do - # Update Template - _updated_template = Templates.update(%Template{new_template| name: updated_name}) - - # Read Template - read_template = Templates.get(new_template.id) - assert read_template.name == updated_name - after - # Delete Template - Templates.delete(new_template) - {:error, _} = Templates.get(new_template.id) - end - end - - @tag :templates - test "template.version crud" do - # Note Better fixture management needed to avoid orphaned elements. - # Note I'm not a fan of multi asserts per test but it simplifies fixture management for now. - - test_run = :os.system_time(:millisecond) - name = "TestTemplate#{test_run}" - test_version_name = "TestVersion#{test_run}" - updated_test_version = "TestVersionUpdated#{test_run}" - - # Create Template - new_template = Templates.create(%Template{name: name}) - template_id = new_template.id - assert template_id != nil - - try do - # Create Version - new_version = Versions.create(%Version{name: test_version_name, template_id: template_id, subject: "Hello World", html_content: "Hello", plain_content: "Hello txt"}) - assert new_version.id != nil - - # Update Version - updated_version = Versions.update(%Version{new_version| name: updated_test_version}) - - # Get Version - read_version = Versions.get(updated_version.template_id, updated_version.id) - assert read_version.name == updated_test_version - - # Delete Version & Confirm - delete_version = Versions.delete(read_version) - assert delete_version == :ok - {:error, _details} = Versions.get(new_version.template_id, new_version.id) - after - Templates.delete(new_template) - end - - {:error, _} = Templates.get(new_template.id) - end - - -end From 52d6d174d6255ea35083c4d69781287019a2c784 Mon Sep 17 00:00:00 2001 From: Keith Brings Date: Sun, 18 Apr 2021 16:36:17 +0700 Subject: [PATCH 3/4] Seperating Template into Dynamic and Legacy to make protocol/guard matching more straight forward for user per type logic. --- lib/sendgrid/email.ex | 3 +- lib/sendgrid/templates/dynamic_template.ex | 66 ++++++++++++++++++++++ lib/sendgrid/templates/legacy_template.ex | 66 ++++++++++++++++++++++ lib/sendgrid/templates/template.ex | 59 ++----------------- lib/sendgrid/templates/templates.ex | 47 +++++++++++---- test/templates_test.exs | 18 +++--- 6 files changed, 186 insertions(+), 73 deletions(-) create mode 100644 lib/sendgrid/templates/dynamic_template.ex create mode 100644 lib/sendgrid/templates/legacy_template.ex diff --git a/lib/sendgrid/email.ex b/lib/sendgrid/email.ex index 1a56c7c..e01cff8 100644 --- a/lib/sendgrid/email.ex +++ b/lib/sendgrid/email.ex @@ -360,7 +360,8 @@ defmodule SendGrid.Email do """ @spec put_template(t, String.t() | SendGrid.Template) :: t - def put_template(%Email{} = email, template = %SendGrid.Template{}), do: put_template(email, template.id) + def put_template(%Email{} = email, template = %SendGrid.LegacyTemplate{}), do: put_template(email, template.id) + def put_template(%Email{} = email, template = %SendGrid.DynamicTemplate{}), do: put_template(email, template.id) def put_template(%Email{} = email, template_id) do %Email{email | template_id: template_id} end diff --git a/lib/sendgrid/templates/dynamic_template.ex b/lib/sendgrid/templates/dynamic_template.ex new file mode 100644 index 0000000..f82c037 --- /dev/null +++ b/lib/sendgrid/templates/dynamic_template.ex @@ -0,0 +1,66 @@ +defmodule SendGrid.DynamicTemplate do + @moduledoc """ + Module to interact with transaction email templates. + """ + alias __MODULE__ + + + @derive Jason.Encoder + defstruct [ + id: nil, + name: nil, + updated_at: nil, + generation: nil, + versions: nil + ] + + @type t :: %DynamicTemplate{ + id: String.t, + name: String.t, + updated_at: DateTime.t | nil, + versions: [Versions.t] + } + + @spec new(Map.t, :json) :: Template.t + def new(json, :json) do + # versions. + versions = case json["versions"] do + nil -> [] + versions when is_list(versions) -> + for version <- versions do + SendGrid.Template.Version.new(version, :json) + end + end + + %DynamicTemplate{ + id: json["id"], + name: json["name"], + updated_at: reformat_sendgrid_date(json["updated_at"]), + versions: versions + } + end + + defp reformat_sendgrid_date(date = %DateTime{}), do: date + defp reformat_sendgrid_date(date) when is_bitstring(date) do + [a,b] = String.split(date) + case DateTime.from_iso8601("#{a} #{b}.0Z") do + {:ok, d, _} -> d + _ -> nil + end + end + defp reformat_sendgrid_date(_), do: nil + + + defimpl Jason.Encoder do + def encode(%SendGrid.DynamicTemplate{} = template, opts) do + raw = template + |> Map.from_struct() + |> Map.drop([:updated_at]) + |> Enum.filter(fn({_k,v}) -> v != nil end) + |> Map.new() + |> Map.put(:generation, :dynamic) + Jason.Encode.map(raw, opts) + end + end + +end diff --git a/lib/sendgrid/templates/legacy_template.ex b/lib/sendgrid/templates/legacy_template.ex new file mode 100644 index 0000000..eb4001f --- /dev/null +++ b/lib/sendgrid/templates/legacy_template.ex @@ -0,0 +1,66 @@ +defmodule SendGrid.LegacyTemplate do + @moduledoc """ + Module to interact with transaction email templates. + """ + alias __MODULE__ + + @derive Jason.Encoder + defstruct [ + id: nil, + name: nil, + updated_at: nil, + versions: nil + ] + + @type t :: %LegacyTemplate{ + id: String.t, + name: String.t, + updated_at: DateTime.t | nil, + versions: [Versions.t] + } + + @spec new(Map.t, :json) :: LegacyTemplate.t + def new(json, :json) do + # versions. + versions = case json["versions"] do + nil -> [] + versions when is_list(versions) -> + for version <- versions do + SendGrid.Template.Version.new(version, :json) + end + end + + %LegacyTemplate{ + id: json["id"], + name: json["name"], + updated_at: reformat_sendgrid_date(json["updated_at"]), + versions: versions + } + end + + defp reformat_sendgrid_date(date = %DateTime{}), do: date + defp reformat_sendgrid_date(date) when is_bitstring(date) do + [a,b] = String.split(date) + case DateTime.from_iso8601("#{a} #{b}.0Z") do + {:ok, d, _} -> d + _ -> nil + end + end + defp reformat_sendgrid_date(_), do: nil + + + defimpl Jason.Encoder do + def encode(%SendGrid.LegacyTemplate{} = template, opts) do + raw = template + |> Map.from_struct() + |> Map.drop([:updated_at]) + |> Enum.filter(fn({_k,v}) -> v != nil end) + |> Map.new() + |> Map.put(:generation, :legacy) + Jason.Encode.map(raw, opts) + end + end + + + +end diff --git a/lib/sendgrid/templates/template.ex b/lib/sendgrid/templates/template.ex index b4e5124..7e51979 100644 --- a/lib/sendgrid/templates/template.ex +++ b/lib/sendgrid/templates/template.ex @@ -1,59 +1,12 @@ defmodule SendGrid.Template do - @moduledoc """ - Module to interact with transaction email templates. - """ - alias __MODULE__ - @generations %{ - "dynamic" => :dynamic, - "legacy" => :legacy, - } - - @derive Jason.Encoder - defstruct [ - id: nil, - name: nil, - updated_at: nil, - generation: nil, - versions: nil - ] - - @type t :: %Template{ - id: String.t, - name: String.t, - updated_at: DateTime.t | nil, - generation: :dynamic | :legacy | String.t, - versions: [Versions.t] - } - - @spec new(Map.t, :json) :: Template.t - def new(json, :json) do - # versions. - versions = case json["versions"] do - nil -> [] - versions when is_list(versions) -> - for version <- versions do - SendGrid.Template.Version.new(version, :json) - end - end - - %Template{ - id: json["id"], - name: json["name"], - updated_at: reformat_sendgrid_date(json["updated_at"]), - generation: @generations[json["generation"]] || json["generation"], - versions: versions - } + def new(%{"generation" => "dynamic"} = json, :json) do + SendGrid.DynamicTemplate.new(json, :json) end - defp reformat_sendgrid_date(date = %DateTime{}), do: date - defp reformat_sendgrid_date(date) when is_bitstring(date) do - [a,b] = String.split(date) - case DateTime.from_iso8601("#{a} #{b}.0Z") do - {:ok, d, _} -> d - _ -> nil - end + + def new(%{"generation" => "legacy"} = json, :json) do + SendGrid.LegacyTemplate.new(json, :json) end - defp reformat_sendgrid_date(_), do: nil -end +end \ No newline at end of file diff --git a/lib/sendgrid/templates/templates.ex b/lib/sendgrid/templates/templates.ex index ea07b0b..eacd6af 100644 --- a/lib/sendgrid/templates/templates.ex +++ b/lib/sendgrid/templates/templates.ex @@ -35,7 +35,7 @@ defmodule SendGrid.Templates do #---------------------------------------- # Pagination #---------------------------------------- - @spec get(SendGrid.Templates.t, SendGrid.query()) :: Templates.t | {:error, [String.t]} | {:error, String.t} + @spec next(SendGrid.Templates.t, SendGrid.query()) :: Templates.t | {:error, [String.t]} | {:error, String.t} def next(self, options \\ []) def next(%SendGrid.Templates{metadata: %SendGrid.MetaData{next: nil}}, options) do nil @@ -67,7 +67,7 @@ defmodule SendGrid.Templates do #---------------------------------------- # CRUD #---------------------------------------- - @spec get(String.t, SendGrid.query()) :: Template.t | {:error, [String.t]} | {:error, String.t} + @spec get(String.t, SendGrid.query()) :: SendGrid.DynamicTemplate.t | SendGrid.LegacyTemplate.t | {:error, [String.t]} | {:error, String.t} def get(identifier, options \\ []) do options = patch_options(options) case SendGrid.get(@base_api_url <> "/#{identifier}", options) do @@ -78,8 +78,19 @@ defmodule SendGrid.Templates do end end - @spec update(Template.t, SendGrid.query()) :: Template.t | {:error, [String.t]} | {:error, String.t} - def update(%Template{} = template, options \\ []) do + @spec update(SendGrid.LegacyTemplate.t | SendGrid.DynamicTemplate.t, SendGrid.query()) :: SendGrid.DynamicTemplate.t | SendGrid.LegacyTemplate.t | {:error, [String.t]} | {:error, String.t} + def update(template, options \\ []) + def update(%SendGrid.LegacyTemplate{} = template, options) do + options = patch_options(options) + case SendGrid.patch(@base_api_url <> "/#{template.id}", template, options) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body } } -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + def update(%SendGrid.DynamicTemplate{} = template, options) do options = patch_options(options) case SendGrid.patch(@base_api_url <> "/#{template.id}", template, options) do { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> @@ -90,8 +101,19 @@ defmodule SendGrid.Templates do end end - @spec create(Template.t, SendGrid.query()) :: Template.t | {:error, [String.t]} | {:error, String.t} - def create(%Template{} = template, options \\ []) do + @spec create(SendGrid.LegacyTemplate.t | SendGrid.DynamicTemplate.t, SendGrid.query()) :: SendGrid.DynamicTemplate.t | SendGrid.LegacyTemplate.t | {:error, [String.t]} | {:error, String.t} + def create(template, options \\ []) + def create(%SendGrid.LegacyTemplate{} = template, options) do + options = patch_options(options) + case SendGrid.post(@base_api_url, template, options) do + { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> + SendGrid.Template.new(response.body, :json) + { :ok, %SendGrid.Response{ body: body }} -> + { :error, body["errors"] || body["error"] } + _ -> { :error, "Unable to communicate with SendGrid API." } + end + end + def create(%SendGrid.DynamicTemplate{} = template, options) do options = patch_options(options) case SendGrid.post(@base_api_url, template, options) do { :ok, response = %SendGrid.Response{ status: status_code } } when status_code in @success_codes -> @@ -102,10 +124,13 @@ defmodule SendGrid.Templates do end end - @spec delete(String.t | Template.t, SendGrid.query()) :: :ok | {:error, [String.t]} | {:error, String.t} + @spec delete(String.t | SendGrid.DynamicTemplate.t | Sendgrid.LegacyTemplate.t, SendGrid.query()) :: :ok | {:error, [String.t]} | {:error, String.t} def delete(template, options \\ []) - def delete(%Template{} = template, options) do - delete(template.id, options) + def delete(%SendGrid.LegacyTemplate{id: id}, options) do + delete(id, options) + end + def delete(%SendGrid.DynamicTemplate{id: id}, options) do + delete(id, options) end def delete(identifier, options) when is_bitstring(identifier) do options = patch_options(options) @@ -119,7 +144,7 @@ defmodule SendGrid.Templates do end end - @spec list(SendGrid.query()) :: [SendGrid.Template.t] | {:error, [String.t]} | {:error, String.t} + @spec list(SendGrid.query()) :: SendGrid.Templates.t | {:error, [String.t]} | {:error, String.t} def list(options \\ []) do options = patch_options(options) fetch = SendGrid.get(@base_api_url, options) @@ -153,4 +178,4 @@ defmodule SendGrid.Templates do end end -end +end \ No newline at end of file diff --git a/test/templates_test.exs b/test/templates_test.exs index 04fe05c..d150854 100644 --- a/test/templates_test.exs +++ b/test/templates_test.exs @@ -11,6 +11,8 @@ defmodule SendGrid.Templates.Test do alias SendGrid.Template alias SendGrid.Template.Versions alias SendGrid.Template.Version + alias SendGrid.DynamicTemplate + alias SendGrid.LegacyTemplate @tag :templates test "generations query param patch" do @@ -22,8 +24,8 @@ defmodule SendGrid.Templates.Test do @tag :templates test "fetch templates" do test_run = :os.system_time(:millisecond) - fixture_a = Templates.create(%Template{name: "TestTemplate#{test_run}a"}) - fixture_b = Templates.create(%Template{name: "TestTemplate#{test_run}b"}) + fixture_a = Templates.create(%LegacyTemplate{name: "TestTemplate#{test_run}a"}) + fixture_b = Templates.create(%DynamicTemplate{name: "TestTemplate#{test_run}b"}) try do actual = Templates.list(query: [page_size: 1]) assert %Templates{} = actual @@ -39,8 +41,8 @@ defmodule SendGrid.Templates.Test do @tag :templates test "paginated fetch templates" do test_run = :os.system_time(:millisecond) - fixture_a = Templates.create(%Template{name: "TestTemplate#{test_run}a"}) - fixture_b = Templates.create(%Template{name: "TestTemplate#{test_run}b"}) + fixture_a = Templates.create(%LegacyTemplate{name: "TestTemplate#{test_run}a"}) + fixture_b = Templates.create(%DynamicTemplate{name: "TestTemplate#{test_run}b"}) try do actual = Templates.list(query: [page_size: 1]) assert %Templates{} = actual @@ -70,12 +72,12 @@ defmodule SendGrid.Templates.Test do updated_name = "UpdatedTemplateName#{test_run}" # Create Template - new_template = Templates.create(%Template{name: name}) + new_template = Templates.create(%DynamicTemplate{name: name}) assert new_template.id != nil try do # Update Template - _updated_template = Templates.update(%Template{new_template| name: updated_name}) + _updated_template = Templates.update(%DynamicTemplate{new_template| name: updated_name}) # Read Template read_template = Templates.get(new_template.id) @@ -98,7 +100,7 @@ defmodule SendGrid.Templates.Test do updated_test_version = "TestVersionUpdated#{test_run}" # Create Template - new_template = Templates.create(%Template{name: name, generation: :legacy}) + new_template = Templates.create(%LegacyTemplate{name: name}) template_id = new_template.id assert template_id != nil @@ -136,7 +138,7 @@ defmodule SendGrid.Templates.Test do updated_test_version = "TestVersionUpdated#{test_run}" # Create Template - new_template = Templates.create(%Template{name: name, generation: :dynamic}) + new_template = Templates.create(%DynamicTemplate{name: name}) template_id = new_template.id assert template_id != nil From c438f27b206e8d87c72fb994238dba8a6a77e17d Mon Sep 17 00:00:00 2001 From: Keith Brings Date: Mon, 12 Dec 2022 08:56:06 +0700 Subject: [PATCH 4/4] - Adding ability to specify exact template version when sending mail. - Exposing (experimental) additional sendgrid email fields. Only injected if specified to avoid impacting existing installations. - Phoenix deps updated. --- .gitignore | 4 +- lib/sendgrid/email.ex | 310 ++++++++++++++++++- mix.exs | 6 +- mix.lock | 15 +- test/support/templates/email/layout.html.eex | 2 +- test/support/templates/email/layout.txt.eex | 2 +- 6 files changed, 311 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index ad1a97f..b3d95e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .idea/ sendgridelixir.iml +sengrid_elixir.iml +sendgrid.iml /_build /cover /deps erl_crash.dump *.ez /doc -.DS_Store \ No newline at end of file +.DS_Store diff --git a/lib/sendgrid/email.ex b/lib/sendgrid/email.ex index e01cff8..11975b5 100644 --- a/lib/sendgrid/email.ex +++ b/lib/sendgrid/email.ex @@ -91,14 +91,22 @@ defmodule SendGrid.Email do bcc: nil, from: nil, reply_to: nil, + reply_to_list: nil, subject: nil, content: nil, template_id: nil, + version_id: nil, substitutions: nil, custom_args: nil, personalizations: nil, send_at: nil, headers: nil, + categories: nil, + batch_id: nil, + asm: nil, + ip_pool_name: nil, + mail_settings: nil, + tracking_settings: nil, attachments: nil, dynamic_template_data: nil, sandbox: false, @@ -111,15 +119,23 @@ defmodule SendGrid.Email do bcc: nil | [recipient], from: nil | recipient, reply_to: nil | recipient, + reply_to_list: nil | [recipient], subject: nil | String.t(), content: nil | [content], template_id: nil | String.t(), + version_id: nil | String.t(), substitutions: nil | substitutions, custom_args: nil | custom_args, personalizations: nil | [Personalization.t()], dynamic_template_data: nil | dynamic_template_data, send_at: nil | integer, headers: nil | headers(), + categories: nil | [String.t], + batch_id: nil | String.t, + asm: nil | asm(), + ip_pool_name: nil | String.t, + mail_settings: nil | mail_settings(), + tracking_settings: nil | tracking_settings(), attachments: nil | [attachment], sandbox: boolean(), __phoenix_view__: nil | atom, @@ -130,6 +146,18 @@ defmodule SendGrid.Email do @type recipient :: %{required(:email) => String.t(), optional(:name) => String.t()} @type content :: %{type: String.t(), value: String.t()} @type headers :: %{String.t() => String.t()} + @type asm :: %{ + required(:group_id) => integer, + optional(:groups_to_display) => [integer] + } + @type mail_settings :: %{ + optional(:bypass_list_management) => bypass_filter(), + optional(:bypass_spam_management) => bypass_filter(), + optional(:bypass_bounce_management) => bypass_filter(), + optional(:bypass_unsubscribe_management) => bypass_filter(), + optional(:footer) => footer(), + optional(:sandbox_mode) => enable_status() + } @type attachment :: %{ required(:content) => String.t(), optional(:type) => String.t(), @@ -138,10 +166,49 @@ defmodule SendGrid.Email do optional(:content_id) => String.t() } + @type enable_status ::%{ + required(:enable) => boolean + } + @type bypass_filter :: enable_status() + @type footer :: %{ + required(:enable) => boolean, + required(:text) => String.t, + required(:html) => String.t + } @type substitutions :: %{String.t() => String.t()} @type custom_args :: %{String.t() => String.t()} @type dynamic_template_data :: %{String.t() => String.t()} + @type tracking_settings :: %{ + optional(:click_tracking) => click_tracking(), + optional(:open_tracking) => open_tracking(), + optional(:subscription_tracking) => subscription_tracking(), + optional(:ganalytics) => google_analytics_tracking() + } + + @type click_tracking :: %{ + optional(:enable) => boolean, + optional(:enable_text) => boolean + } + @type open_tracking :: %{ + optional(:enable) => boolean, + optional(:substitution_tag) => nil | String.t + } + @type subscription_tracking :: %{ + optional(:enable) => boolean, + optional(:text) => String.t, + optional(:html) => String.t, + optional(:substitution_tag) => String.t + } + @type google_analytics_tracking :: %{ + optional(:enable) => boolean, + optional(:utm_source) => String.t, + optional(:utm_medium) => String.t, + optional(:utm_content) => String.t, + optional(:utm_campaign) => String.t + } + + @doc """ Builds an an empty email to compose on. @@ -285,6 +352,152 @@ defmodule SendGrid.Email do %Email{email | reply_to: address(reply_to_address, reply_to_name)} end + + + @doc """ + Sets the `reply_to_list` field for email. You may not use reply_to_list and reply_to at the same time. + """ + @spec put_reply_to_list(t, [String.t()]) :: t + def put_reply_to_list(%Email{} = email, reply_to_addresses) do + list = Enum.map(reply_to_addresses, fn(v) -> + case v do + {address, name} -> address(address, name) + address -> address(address) + end + end) + %Email{email | reply_to_list: list} + end + + @doc """ + Set/Replace email categories. + """ + def put_categories(%Email{} = email, categories) do + %Email{email| categories: categories} + end + + @doc """ + Add/set email category. + """ + def put_category(%Email{} = email, category) do + case email.categories do + nil -> %Email{email| categories: [category]} + v -> %Email{email| categories: Enum.uniq(v ++ [category])} + end + end + + @doc """ + Set batch_id + """ + def put_batch(%Email{} = email, value) do + %Email{email| batch_id: value} + end + + @doc """ + Set asm + """ + def put_asm(%Email{} = email, value) do + %Email{email| asm: value} + end + + + @doc """ + Set ip_pool_name + """ + def put_ip_pool(%Email{} = email, value) do + %Email{email| ip_pool_name: value} + end + + # Initialize mail_settings + defp init_mail_settings(%Email{mail_settings: nil} = email) do + %Email{email| mail_settings: %{}} + end + defp init_mail_settings(%Email{} = email), do: email + + @doc """ + Set email.mail_settings.bypass_list_management + """ + def configure_list_management_bypass(%Email{} = email, enable) do + init_mail_settings(email) + |> put_in([Access.key(:mail_settings), :bypass_list_management], %{enable: enable}) + end + + @doc """ + Set email.mail_settings.bypass_spam_management + """ + def configure_spam_management_bypass(%Email{} = email, enable) do + init_mail_settings(email) + |> put_in([Access.key(:mail_settings), :bypass_spam_management], %{enable: enable}) + end + + @doc """ + Set email.mail_settings.bypass_bounce_management + """ + def configure_bounce_management_bypass(%Email{} = email, enable) do + init_mail_settings(email) + |> put_in([Access.key(:mail_settings), :bypass_bounce_management], %{enable: enable}) + end + + @doc """ + Set email.mail_settings.bypass_unsubscribe_management + """ + def configure_unsubscribe_management_bypass(%Email{} = email, enable) do + init_mail_settings(email) + |> put_in([Access.key(:mail_settings), :bypass_unsubscribe_management], %{enable: enable}) + end + + @doc """ + Set email.mail_settings.bypass_unsubscribe_management + """ + def put_footer(%Email{} = email, footer) do + init_mail_settings(email) + |> put_in([Access.key(:mail_settings), :footer], footer) + end + + @doc """ + Set entire mail_settings field, replacing any previous settings. + """ + def put_mail_settings(%Email{} = email, value) do + %Email{mail_settings: value} + end + + + # Initialize track_settings + defp init_tracking_settings(%Email{tracking_settings: nil} = email) do + %Email{email| tracking_settings: %{}} + end + defp init_tracking_settings(%Email{} = email), do: email + + def configure_click_tracking(%Email{} = email, value) do + email + |> init_tracking_settings() + |> put_in([Access.key(:tracking_settings), :click_tracking], value) + end + + def configure_open_tracking(%Email{} = email, value) do + email + |> init_tracking_settings() + |> put_in([Access.key(:tracking_settings), :open_tracking], value) + end + + def configure_subscription_tracking(%Email{} = email, value) do + email + |> init_tracking_settings() + |> put_in([Access.key(:tracking_settings), :subscription_tracking], value) + end + + def configure_google_analytics(%Email{} = email, value) do + email + |> init_tracking_settings() + |> put_in([Access.key(:tracking_settings), :ganalytics], value) + end + + def put_tracking_settings(%Email{} = email, value) do + email + |> put_in([Access.key(:tracking_settings)], value) + end + + + @doc """ Sets the `subject` field for the email. @@ -365,7 +578,22 @@ defmodule SendGrid.Email do def put_template(%Email{} = email, template_id) do %Email{email | template_id: template_id} end + + @doc """ + Uses a predefined SendGrid template version for the email. + ## Examples + + Email.put_template_version(%Email{}, "the_template_version_id") + + """ + @spec put_template_version(t, String.t()) :: t + def put_template_version(%Email{} = email, version_id) when is_bitstring(version_id) do + %Email{email | version_id: version_id} + end + + + @doc """ Adds a substitution value to be used with a template. @@ -655,7 +883,7 @@ defmodule SendGrid.Email do @doc """ Sets the email to be sent with sandbox mode enabled or disabled. - The sandbox mode will default to what is explicity configured with + The sandbox mode will default to what is explicitly configured with SendGrid's configuration. """ @spec set_sandbox(t(), boolean()) :: t() @@ -696,24 +924,72 @@ defmodule SendGrid.Email do end defimpl Jason.Encoder do + + defp conditional_insert(params, email, field, as_field \\ nil) do + cond do + v = Map.get(email, field) -> put_in(params, [as_field || field], v) + :else -> params + end + end + def encode(%Email{personalizations: [_ | _]} = email, opts) do params = %{ - personalizations: email.personalizations, - from: email.from, - subject: email.subject, - content: email.content, - reply_to: email.reply_to, - send_at: email.send_at, - template_id: email.template_id, - attachments: email.attachments, - headers: email.headers, - mail_settings: %{ - sandbox_mode: %{ - enable: Application.get_env(:sendgrid, :sandbox_enable, email.sandbox) - } - } - } - + personalizations: email.personalizations, + from: email.from, + subject: email.subject, + content: email.content, + send_at: email.send_at, + attachments: email.attachments, + headers: email.headers, + } + # Template + |> then( + fn(params) -> + cond do + email.template_id && email.version_id -> put_in(params, [:template_id], email.template_id <> "." <> email.version_id) + email.template_id -> put_in(params, [:template_id], email.template_id) + email.version_id -> raise ArgumentError, "You must specify template if specifying template version" + :else -> params + end + end) + # Reply List + |> then( + fn(params) -> + cond do + email.reply_to_list && email.reply_to -> + raise ArgumentError, "You may not set reply_to_list and reply_to at the same time." + v = email.reply_to -> put_in(params, [:reply_to], v) + v = email.reply_to_list -> put_in(params, [:reply_to_list], v) + :else -> params + end + end) + # Mail Settings + |> conditional_insert(email, :mail_settings) + # Track Settings + |> conditional_insert(email, :tracking_settings, :track_settings) + # Categories + |> conditional_insert(email, :categories) + # Batch + |> conditional_insert(email, :batch_id) + # ASM + |> conditional_insert(email, :asm) + # IP Pool + |> conditional_insert(email, :ip_pool_name) + # sandbox_mode + |> then( + fn(params) -> + # Insure partially populated. + params + |> update_in([:mail_settings], &(&1 || %{})) + |> update_in([:mail_settings, :sandbox_mode], &(&1 || %{})) + |> update_in([:mail_settings, :sandbox_mode, :enable], fn(p) -> + cond do + is_boolean(p) -> p + :else -> Application.get_env(:sendgrid, :sandbox_enable, email.sandbox) + end + end) + end) + Jason.Encode.map(params, opts) end diff --git a/mix.exs b/mix.exs index 11df5f5..3e2c8fa 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule SendGrid.Mixfile do def project do [app: :sendgrid, - version: "2.0.0", + version: "2.0.1", elixir: "~> 1.4", package: package(), compilers: compilers(Mix.env), @@ -45,8 +45,8 @@ defmodule SendGrid.Mixfile do {:earmark, "~> 1.2", only: :dev}, {:ex_doc, "~> 0.19", only: :dev}, {:jason, "~> 1.1"}, - {:phoenix, "~> 1.2", only: :test}, - {:phoenix_html, "~> 2.9", only: :test}, + {:phoenix, "~> 1.6", only: :test}, + {:phoenix_html, "~> 3.2", only: :test}, {:tesla, "~> 1.2"} ] end diff --git a/mix.lock b/mix.lock index 8b026ab..4621c14 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "4bba10c6f267a0dd127d687d1295f6a11af6a7f160cc0e261c46f1962a98d7d8"}, "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, @@ -12,16 +13,20 @@ "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"}, "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm", "33dd09e615daab5668c15cc3a33829892728fdbed910ab0c0a0edb06b45fc54d"}, + "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], []}, "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"}, - "phoenix": {:hex, :phoenix, "1.2.4", "4172479b5e21806a5e4175b54820c239e0d4effb0b07912e631aa31213a05bae", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.4 or ~> 1.3.3 or ~> 1.2.4 or ~> 1.1.8 or ~> 1.0.5", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "e75dcda4cce7acdb72a10d4780914d288b5c728f6ea58fd85cf41671cd56c35b"}, - "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f090e3a75de4efd72ae2b9b016909fe673512ef04e6b72d7bf039031a64d4a52"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm", "6f9193364c5de86b85e8d3a80294a134aecf6c5618adcbae668608749e00a7f7"}, - "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "141058cca1fa800128391ece7f442f71a7b42a7411e6eaa56dc8f85283c8dde7"}, + "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, + "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], [], "hexpm", "519bc209e4433961284174c497c8524c001e285b79bdf80212b47a1f898084cc"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "tesla": {:hex, :tesla, "1.2.0", "9e2469c1bcdb0cc8fe5fd3e9208432f3fee8e439a44f639d8ce72bcccd6f3566", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "9d6f9957d90ff2fdd370d62a3704b101dc0d11be73994b3794a43c37f01c1489"}, } diff --git a/test/support/templates/email/layout.html.eex b/test/support/templates/email/layout.html.eex index fab2a07..250b991 100644 --- a/test/support/templates/email/layout.html.eex +++ b/test/support/templates/email/layout.html.eex @@ -1,2 +1,2 @@ HTML LAYOUT -<%= render @view_module, @view_template, assigns %> \ No newline at end of file +<%= @inner_content %> \ No newline at end of file diff --git a/test/support/templates/email/layout.txt.eex b/test/support/templates/email/layout.txt.eex index 459f4a3..00cd2d2 100644 --- a/test/support/templates/email/layout.txt.eex +++ b/test/support/templates/email/layout.txt.eex @@ -1,2 +1,2 @@ TEXT LAYOUT -<%= render @view_module, @view_template, assigns %> \ No newline at end of file +<%= @inner_content %> \ No newline at end of file