Skip to content

Instantly share code, notes, and snippets.

@seanwash
Last active April 3, 2018 11:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seanwash/c7ffb8753e2f7e2da873502328c8c83f to your computer and use it in GitHub Desktop.
Save seanwash/c7ffb8753e2f7e2da873502328c8c83f to your computer and use it in GitHub Desktop.
An example of pattern matching with guard clauses.
defmodule Dk.Utils.Access do
@moduledoc """
A collection of functions for working with deeply nested data.
"""
def deep_update_in(list, [], fun) when is_list(list) do
Enum.map(list, &deep_update_in(&1, [], fun))
end
def deep_update_in(the_end, [], fun) do
fun.(the_end)
end
def deep_update_in(map, [key | rest], fun) when is_map(map) do
Map.update(map, key, nil, &deep_update_in(&1, rest, fun))
end
def deep_update_in(list, path, fun) when is_list(list) do
Enum.map(list, &deep_update_in(&1, path, fun))
end
end
defmodule Dk.Presenter.ProductPresenter do
@moduledoc """
This module is responsible for augmenting a Product or a list of Products with presentational
fields based off of the given user's account type and other outside factors.
## Usage
Presenters should be used after a collection of products have been compiled, but before sending
that data off to a view for serialization or Eex templates for building pages.
"""
alias Dk.Repo
alias Dk.Accounts.Users
alias Dk.Pricing
alias Dk.Formatter
alias Dk.Utils.Access
def augment_in(parent, key, user \\ nil, opts \\ [])
@doc """
Augments a nested list of products located at the given path within a %Scrivener.Page{}.
struct.
## Examples
iex> augment_in(
%Scrivener.Page{entries: [%Store{products: %Product{}}]},
[:products]
)
%Scrivener.Page{}
"""
@spec augment_in(struct, list, struct, keyword) :: struct
def augment_in(%Scrivener.Page{entries: entries} = page, path, user, opts) do
Map.put(
page,
:entries,
Access.deep_update_in(entries, path, fn product -> augment(product, user, opts) end)
)
end
@doc """
Augments a nested list of products located at the given path within the given list of structs.
## Examples
iex> augment_in(
[%Campaign{issues: [%Issue{issue_products: [%IssueProduct{product: %Product{}}]}]}, ...],
[:issues, :issue_products, :product]]
)
%Campaign{}
"""
@spec augment_in(list, list, struct, keyword) :: struct
def augment_in(parent, path, user, opts) when is_list(parent) do
Access.deep_update_in(parent, path, fn product -> augment(product, user, opts) end)
end
@doc """
Augments a nested list of products located at the given path within the given struct.
## Examples
iex> augment_in(
%Campaign{issues: [%Issue{issue_products: [%IssueProduct{product: %Product{}}]}]},
[:issues, :issue_products, :product]]
)
%Campaign{}
"""
@spec augment_in(struct, list, struct, keyword) :: struct
def augment_in(parent, path, user, opts) when is_map(parent) and is_list(path) do
Access.deep_update_in(parent, path, fn product -> augment(product, user, opts) end)
end
@doc """
Augments a single product found in the given struct.
## Examples
iex> augment_in(
%IssueProduct{product: %Product{}},
:product
)
%IssueProduct{}
"""
@spec augment_in(struct | map, atom, struct, keyword) :: struct | map
def augment_in(parent, key, user, opts) when is_map(parent) and is_atom(key) do
product =
Map.get(parent, key)
|> augment(user, opts)
Map.put(parent, key, product)
end
def augment(entries, user \\ nil, opts \\ [])
@doc """
Augments each product contained in the given list.
## Examples
iex> augment([%Product{}, %Product{}], %User{})
[%Product{}, %Product{}]
"""
@spec augment(list, struct, keyword) :: list
def augment(products, user, opts) when is_list(products) do
products
|> Enum.map(&augment(&1, user, opts))
end
@doc """
Augments the entries belonging to the given %Scrivener.Page{} struct.
## Examples
iex> augment(%Scrivener.Page{entries: [%Product{}, %Product{}]}, %User{})
%Scrivener.Page{}
"""
@spec augment(struct, struct, keyword) :: struct
def augment(%Scrivener.Page{entries: entries} = page, user, opts) do
augmented_entries =
entries
|> Enum.map(&augment(&1, user, opts))
Map.put(page, :entries, augmented_entries)
end
@doc """
Adds various virtual attributes to the given Product map based on the current user.
## Examples
iex> augment(%Product{}, %User{})
%Product{}
"""
@spec augment(struct | map, struct, keyword) :: struct | map
def augment(product, user, opts) when is_map(product) do
product
|> modify_purchase_prices()
|> inject_trade_price(user)
|> inject_product_favorite(user)
|> inject_distance_in_miles(opts)
end
defp modify_purchase_prices(product) do
%{
product
| base_price_in_cents: Pricing.base_price_in_cents_for_product(product),
sale_price_in_cents: Pricing.sale_price_in_cents_for_product(product)
}
end
defp inject_trade_price(product, user) do
with true <- user && Users.user_eligible_for_trade_discount?(user),
trade_price =
Pricing.user_price_in_cents_for_product(
product,
user,
trade_user_discount_percent: product.store.trade_user_discount_percent
),
true <-
trade_price > 0 &&
trade_price < Pricing.base_purchase_price_in_cents_for_product(product) do
product
|> Map.put(:sale_price_in_cents, trade_price)
|> Map.put(:trade_user_discount_applied, true)
else
_ ->
Map.put(product, :trade_user_discount_applied, false)
end
end
defp inject_product_favorite(product, nil) do
Map.put(product, :favorite, nil)
end
defp inject_product_favorite(product, user) do
favorite = Dk.Engagement.find_favorite(product.id, user.id)
Map.put(product, :favorite, favorite)
end
defp inject_distance_in_miles(product, opts) do
location = opts[:location] || nil
if is_nil(location) do
Map.put(product, :distance_in_miles, nil)
else
do_inject_distance_in_miles(product, location)
end
end
defp do_inject_distance_in_miles(product, point_b) do
point_a = product.addresses |> List.last() |> Map.get(:point)
distance =
case point_a do
nil ->
nil
_ ->
[lng, lat] = String.split(point_b, ",")
point_b = %Geo.Point{coordinates: {lng, lat}, srid: 4326}
calculate_distance(point_a, point_b)
end
Map.put(product, :distance_in_miles, distance)
end
defp calculate_distance(point_a, point_b) do
result =
Repo.query("SELECT ST_Distance(ST_GeogFromText($1), ST_GeogFromText($2))", [
Geo.WKT.encode(point_a),
Geo.WKT.encode(point_b)
])
case result do
{:error, _reason} ->
nil
{:ok, result} ->
result.rows
|> List.first()
|> List.first()
|> Formatter.Distance.meters_to_formatted_miles()
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment