Skip to content

CI infrastructure, dependency updates #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
inputs: [
"{mix, .formatter, .credo}.exs",
"{config,lib,test}/**/*.{ex,exs}"
],
line_length: 100
]
86 changes: 86 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Elixir CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build_and_test:
name: Build and test
runs-on: ubuntu-latest
strategy:
matrix:
elixir: ["1.10.4"]
otp: [23]
cache: [1]
services:
db:
image: postgres:11
ports: ['5432:5432']
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Set up Elixir
uses: actions/setup-elixir@v1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Restore dependencies cache
id: mix-deps-cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-${{ matrix.cache }}-mix

- name: Restore build cache
uses: actions/cache@v1
with:
path: _build
key: cache-${{ runner.os }}-dialyzer_build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build-

- name: Install Mix Dependencies
if: steps.mix-deps-cache.outputs.cache-hit != 'true'
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get

- name: Credo
run: mix credo --strict

- name: Check formatting
run: mix format --check-formatted

- name: Restore PLT Cache
uses: actions/cache@v1
id: plt-cache
with:
path: priv/plts
key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles('**/mix.lock') }}
restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build-

- name: Create PLTs
if: steps.plt-cache.outputs.cache-hit != 'true'
run: |
mkdir -p plts
mix dialyzer --plt

- name: Run dialyzer
run: mix dialyzer

- name: Run tests
run: mix test

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
erl_crash.dump
*.ez
*.swp
/plts/*.plt
/plts/*.plt.hash
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
elixir 1.10.4
2 changes: 1 addition & 1 deletion examples/simplesend.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ from = "Elixir SparkPost <elixir@sparkpostbox.com>"
SparkPost.send(
to: to,
from: from,
subject: "My first Elixir email",
subject: "My first Elixir email",
text: "This is the boring version of the email body",
html: "This is the <strong>tasty</strong> <em>rich</em> version of the <a href=\"https://www.sparkpost.com/\">email</a> body."
)
19 changes: 11 additions & 8 deletions lib/address.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,27 @@ defmodule SparkPost.Address do
- `%{name: ..., email: ...}`
"""
def to_address(email) when is_binary(email) do
parse_address(email)
parse_address(email)
end

def to_address(%{name: name, email: email})do
def to_address(%{name: name, email: email}) do
%__MODULE__{name: name, email: email}
end

def to_address(%{email: email})do
def to_address(%{email: email}) do
%__MODULE__{email: email}
end

defp parse_address(addr) when is_binary(addr) do
case Regex.run(~r/\s*(.+)\s+<(.+@.+)>\s*$/, addr) do
[_, name, email] -> %__MODULE__{ name: name, email: email }
nil -> case Regex.run(~r/\s*(.+@.+)\s*$/, addr) do
[_, email] -> %__MODULE__{ email: email }
nil -> raise __MODULE__.FormatError, message: "Invalid email address: #{addr}"
end
[_, name, email] ->
%__MODULE__{name: name, email: email}

nil ->
case Regex.run(~r/\s*(.+@.+)\s*$/, addr) do
[_, email] -> %__MODULE__{email: email}
nil -> raise __MODULE__.FormatError, message: "Invalid email address: #{addr}"
end
end
end
end
6 changes: 2 additions & 4 deletions lib/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,10 @@ defmodule SparkPost.Content do
end

def to_content(%SparkPost.Content.Inline{} = content) do
%{ content |
from: SparkPost.Address.to_address(content.from)}
%{content | from: SparkPost.Address.to_address(content.from)}
end

def to_content(content) when is_map(content) do
%{ struct(SparkPost.Content.Inline, content) |
from: SparkPost.Address.to_address(content.from)}
%{struct(SparkPost.Content.Inline, content) | from: SparkPost.Address.to_address(content.from)}
end
end
15 changes: 8 additions & 7 deletions lib/content/inline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ defmodule SparkPost.Content.Inline do
"""

defstruct from: :required,
reply_to: nil,
headers: nil,
subject: :required,
text: nil,
html: nil,
attachments: nil,
inline_images: nil
reply_to: nil,
headers: nil,
subject: :required,
text: nil,
html: nil,
attachments: nil,
inline_images: nil

@doc """
Convert a raw "from" field into a %SparkPost.Address{} object.
"""
def convert_from_field(%SparkPost.Endpoint.Error{} = content), do: content

def convert_from_field(%__MODULE__{} = content) do
%{content | from: SparkPost.Address.to_address(content.from)}
end
Expand Down
41 changes: 24 additions & 17 deletions lib/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule SparkPost.Endpoint do
- `:get`
- `:head`
- `:options`
- `:patch`
- `:patch`
- `:post`
- `:put`
- `endpoint`: SparkPost API endpoint as string ("transmissions", "templates", ...)
Expand All @@ -32,27 +32,29 @@ defmodule SparkPost.Endpoint do
"id" => "102258558346809186", "name" => "102258558346809186",
"state" => "Success"}, ...], status_code: 200}
"""
def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ [], decode_results \\ true) do
def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ [], decode_results \\ true) do
url = Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint

{:ok, request_body} = encode_request_body(body)

request_headers = if method in [:get, :delete] do
request_headers =
if method in [:get, :delete] do
headers
else
Map.merge(headers, %{"Content-Type": "application/json"})
end
|> Map.merge(base_request_headers())

request_options = options
|> Keyword.put(:timeout, Application.get_env(:sparkpost, :http_timeout, 30000))
|> Keyword.put(:recv_timeout, Application.get_env(:sparkpost, :http_recv_timeout, 8000))
request_options =
options
|> Keyword.put(:timeout, Application.get_env(:sparkpost, :http_timeout, 30_000))
|> Keyword.put(:recv_timeout, Application.get_env(:sparkpost, :http_recv_timeout, 8000))

HTTPoison.request(method, url, request_body, request_headers, request_options)
|> handle_response(decode_results)
end

def marshal_response(response, struct_type, subkey\\nil)
def marshal_response(response, struct_type, subkey \\ nil)

@doc """
Extract a meaningful structure from a generic endpoint response:
Expand All @@ -70,17 +72,21 @@ defmodule SparkPost.Endpoint do
response
end

defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, decode_results) when code >= 200 and code < 300 do
defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, decode_results)
when code >= 200 and code < 300 do
decoded_body = decode_response_body(body)
if decode_results do

if decode_results && Map.has_key?(decoded_body, :results) do
%SparkPost.Endpoint.Response{status_code: code, results: decoded_body.results}
else
%SparkPost.Endpoint.Response{status_code: code, results: decoded_body}
end
end

defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, _decode_results) when code >= 400 do
defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, _decode_results)
when code >= 400 do
decoded_body = decode_response_body(body)

if Map.has_key?(decoded_body, :errors) do
%SparkPost.Endpoint.Error{status_code: code, errors: decoded_body.errors}
end
Expand All @@ -90,24 +96,25 @@ defmodule SparkPost.Endpoint do
%SparkPost.Endpoint.Error{status_code: nil, errors: [reason]}
end

defp base_request_headers() do
defp base_request_headers do
{:ok, version} = :application.get_key(:sparkpost, :vsn)

%{
"User-Agent": "elixir-sparkpost/" <> to_string(version),
"Authorization": Application.get_env(:sparkpost, :api_key)
Authorization: Application.get_env(:sparkpost, :api_key)
}
end

# Do not try to remove nils from an empty map
# Do not try to remove nils from an empty map
defp encode_request_body(body) when is_map(body) and map_size(body) == 0, do: {:ok, ""}

defp encode_request_body(body) do
body |> Washup.filter |> Poison.encode
body |> Washup.filter() |> Poison.encode()
end

defp decode_response_body(body) when byte_size(body) == 0, do: ""

defp decode_response_body(body) do
# TODO: [key: :atoms] is unsafe for open-ended structures such as
# metadata and substitution_data
body |> Poison.decode!([keys: :atoms])
body |> Poison.decode!(keys: :atoms)
end
end
14 changes: 8 additions & 6 deletions lib/mockserver.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
defmodule SparkPost.MockServer do
@moduledoc false

def create_json(endpoint\\"transmission") do
def create_json(endpoint \\ "transmission") do
File.read!("test/data/create#{endpoint}.json")
end

def create_fail_json(endpoint\\"transmission") do
def create_fail_json(endpoint \\ "transmission") do
File.read!("test/data/create#{endpoint}fail.json")
end

def list_json(endpoint\\"transmission") do
def list_json(endpoint \\ "transmission") do
File.read!("test/data/list#{endpoint}.json")
end

def get_json(endpoint\\"transmission") do
def get_json(endpoint \\ "transmission") do
File.read!("test/data/#{endpoint}.json")
end

Expand All @@ -34,10 +34,12 @@ defmodule SparkPost.MockServer do
end

def mk_http_resp(status_code, body) do
fn (_method, _url, _body, _headers, _opts) -> {:ok, %HTTPoison.Response{status_code: status_code, body: body}} end
fn _method, _url, _body, _headers, _opts ->
{:ok, %HTTPoison.Response{status_code: status_code, body: body}}
end
end

def mk_error(reason) do
fn (_method, _url, _body, _headers, _opts) -> {:error, %HTTPoison.Error{reason: reason}} end
fn _method, _url, _body, _headers, _opts -> {:error, %HTTPoison.Error{reason: reason}} end
end
end
22 changes: 11 additions & 11 deletions lib/recipient.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ defmodule SparkPost.Recipient do
"""

defstruct address: :required,
return_path: nil,
tags: nil,
metadata: nil,
substitution_data: nil
return_path: nil,
tags: nil,
metadata: nil,
substitution_data: nil

alias SparkPost.{Recipient, Address}
alias SparkPost.{Address, Recipient}

@doc """
Convenience conversions to `[ %SparkPost.Recipient{} ]` from:
Expand All @@ -29,12 +29,11 @@ defmodule SparkPost.Recipient do
end

def to_recipient_list(email_list) when is_list(email_list) do
Enum.map(email_list, fn (recip) -> to_recipient(recip)
end)
Enum.map(email_list, fn recip -> to_recipient(recip) end)
end

def to_recipient_list(email) when is_binary(email) do
[ to_recipient(email) ]
[to_recipient(email)]
end

@doc """
Expand All @@ -58,15 +57,16 @@ defmodule SparkPost.Recipient do

def to_recipient(%{address: address} = struc) do
struct(__MODULE__, %{
struc | address: Address.to_address(address)
struc
| address: Address.to_address(address)
})
end

def to_recipient(%{name: _name, email: _email} = struc) do
%__MODULE__{ address: Address.to_address(struc) }
%__MODULE__{address: Address.to_address(struc)}
end

def to_recipient(%{email: _} = struc) do
%__MODULE__{ address: Address.to_address(struc) }
%__MODULE__{address: Address.to_address(struc)}
end
end
Loading