Sponsor us and add your link here.
Notesclub

Design Patterns in Elixir

livebook.livemd

Design Patterns in Elixir

Mix.install([
  {:elixir_xml_to_map, "~> 3.0"},
  {:jason, "~> 1.4.0"},
  {:csv, "~> 3.0.4"},
  {:ecto_sql, "~> 3.0"},
  {:postgrex, ">= 0.0.0"},
  {:elixir_trees, github: "crisefd/elixir_trees"}
])

Protocols for File parsing

defmodule FileHelpers do
  def file_to_stream!(filename) do
    filename |> Path.expand(__DIR__) |> File.stream!()
  end

  def file_to_binary!(filename) do
    filename |> Path.expand(__DIR__) |> File.read!()
  end
end
defprotocol Input do
  def parse!(filename)
end

defprotocol Output do
  def to_json(file)
end
defmodule Format.XML do
  defstruct [:version, :content]
end

defmodule Format.CSV do
  defstruct [:headers, :rows]
end

defimpl Input, for: Format.XML do
  import FileHelpers

  def parse!(filename) do
    content =
      filename
      |> file_to_binary!()
      |> XmlToMap.naive_map()

    %Format.XML{version: "1.0", content: content}
  end
end

defimpl Input, for: Format.CSV do
  import FileHelpers

  def parse!(filename) do
    stream =
      filename
      |> file_to_stream!()
      |> CSV.decode!()

    headers = Enum.take(stream, 1)
    rows = stream |> Stream.drop(1) |> Enum.to_list()

    %Format.CSV{headers: headers, rows: rows}
  end
end
csv = 

Use macro for parsing

defmodule Input do
  defstruct [:content]

  defmacro __using__(_opts) do
    quote do
      @callback parse!(filename :: binary) :: map()

      def file_to_stream!(filename) do
        filename |> Path.expand(__DIR__) |> File.stream!()
      end

      def file_to_binary!(filename) do
        filename |> Path.expand(__DIR__) |> File.read!()
      end
    end
  end
end

defmodule Input.TXT do
  use Input
end

defmodule Input.XML do
  use Input

  def parse!(filename) do
    content =
      filename
      |> file_to_binary!()
      |> XmlToMap.naive_map()

    %Input{content: content}
  end
end

defmodule Input.CSV do
  use Input

  def parse!(filename) do
    content =
      filename
      |> file_to_stream!()
      |> CSV.decode!(headers: true)
      |> Enum.to_list()

    %Input{content: content}
  end
end
"input.xml" |> Path.expand(__DIR__) |> File.read!() |> XmlToMap.naive_map()
xml = Input.XML.parse!("./input.xml")
csv = Input.CSV.parse!("./input.csv")
txt = Input.TXT.parse!("./input.txt")
%{xml: xml, csv: csv, txt: txt}
defmodule Output.Simple.JSON do
  def encode!(input) do
    case input do
      %Input.CSV{} = csv -> from_csv(csv)
      %Input.XML{} = xml -> from_xml(xml)
    end
  end

  defp from_csv(csv) do
    # Transform CSV to JSON
  end

  defp from_xml(xml) do
    # Tranform XML to JSON
  end
end
defprotocol Output.JSON do
  def encode!(input)
end
defimpl Output.JSON, for: Input.XML do
  def encode!(input) do
    input |> Map.from_struct() |> Jason.encode!()
  end
end

defimpl Output.JSON, for: Input.CSV do
  def encode!(input) do
    input.data
    |> Enum.map(fn row ->
      input.headers |> Enum.zip(row) |> Map.new()
    end)
    |> Jason.encode!()
  end
end
xml_out = Output.JSON.encode!(xml)
csv_out = Output.JSON.encode!(csv)
%{xml: xml_out, csv: csv_out}

Decorator Pattern

defmodule ListHouses do
  import Ecto.Query

  def list(filter) do
    base_query()
    |> with_type(filter)
    |> with_available(filter)
    |> Repo.all()
  end

  defp base_query(), do: from(h in House)

  defp with_type(query, %{type: type})
       when type in [:apartment, :house] do
    where(query, [h], h.type == ^type)
  end

  defp with_type(query, _filter), do: query

  defp with_available(query, %{available: available})
       when is_boolean(available) do
    where(query, [h], h.available == ^available)
  end

  defp with_available(query, _filter), do: query
end

Composite Pattern

defmodule Order do
  defstruct [:products]

  def total(order) do
    order.products
    |> Enum.map(&Price.total/1)
    |> List.flatten()
    |> Enum.sum()
  end
end

defmodule Product do
  defstruct [:name, :price, addons: []]
end

defmodule Shipping do
  defstruct [:price]
end

defprotocol Price do
  def total(item)
end

defimpl Price, for: Product do
  def total(product) do
    # Not tail-call optimised. Use only for small trees.
    [product.price | Enum.map(product.addons, &Price.total/1)]
  end
end

defimpl Price, for: Shipping do
  def total(shipping), do: shipping.price
end
order = %Order{
  products: [
    %Product{
      name: "Charging Cable",
      price: 10
    },
    %Product{
      name: "Smartphone",
      price: 800,
      addons: [
        %Product{
          name: "Name Engraving",
          price: 20,
          addons: [
            %Product{
              name: "Golden Letters",
              price: 3
            }
          ]
        }
      ]
    },
    %Shipping{
      price: 5
    }
  ]
}

Order.total(order)