defmodule StructuredIO.Enumerator do @moduledoc """ Provides an `Enumerable` implementation for `StructuredIO`. Call `StructuredIO.enumerate_with/3`or `StructuredIO.enumerate_with/4` instead of invoking this module directly. Note that enumeration is not a purely functional operation; it consumes data elements from the underlying `StructuredIO` process. ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foo" :ok iex> StructuredIO.write structured_io, ...> "bar" :ok iex> StructuredIO.write structured_io, ...> "baz" :ok iex> {:ok, ...> enumerator} = StructuredIO.Enumerator.new(%{process: structured_io, ...> function: :read_between, ...> additional_arguments: ["", ...> ""]}) iex> Enum.map enumerator, &String.upcase/1 ["FOO", "BAR", "BAZ"] iex> Enum.map enumerator, &String.upcase/1 [] iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foo" :ok iex> StructuredIO.write structured_io, ...> "bar" :ok iex> StructuredIO.write structured_io, ...> "baz" :ok iex> {:ok, ...> enumerator} = StructuredIO.Enumerator.new(%{process: structured_io, ...> function: :read_between, ...> additional_arguments: ["", ...> ""]}) iex> Enum.count enumerator 3 iex> Enum.count enumerator 0 iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foo" :ok iex> StructuredIO.write structured_io, ...> "bar" :ok iex> StructuredIO.write structured_io, ...> "baz" :ok iex> {:ok, ...> enumerator} = StructuredIO.Enumerator.new(%{process: structured_io, ...> function: :read_between, ...> additional_arguments: ["", ...> ""]}) iex> Enum.member? enumerator, "bar" true iex> Enum.member? enumerator, "foo" false iex> Enum.member? enumerator, "bar" false iex> Enum.member? enumerator, "baz" false """ @behaviour StructuredIO.Enumerator.Behaviour # TODO: Consider eliminating needless fields in v2: `:process` and `:additional_arguments` @enforce_keys ~w{process function additional_arguments arguments}a defstruct process: nil, function: nil, timeout: nil, additional_arguments: [], arguments: nil @typedoc """ A `#{inspect __MODULE__}` struct. """ @type t :: %__MODULE__{process: GenServer.server, function: atom, timeout: nil | timeout, additional_arguments: [any], arguments: [any]} defimpl Enumerable do # Use the default implementation of Enumerable.count/1. def count(_enumerator), do: {:error, __MODULE__} # Use the default implementation of Enumerable.member?/2. def member?(_enumerator, _element), do: {:error, __MODULE__} def reduce(_enumerator, {:halt, acc}, _fun), do: {:halted, acc} def reduce(enumerator, {:suspend, acc}, fun) do {:suspended, acc, &reduce(enumerator, &1, fun)} end def reduce(enumerator, {:cont, acc}, fun) do case apply(StructuredIO, enumerator.function, enumerator.arguments) do {:error, _}=error -> {:done, error} "" -> {:done, acc} element -> reduce(enumerator, fun.(element, acc), fun) end end # Use the default implementation of Enumerable.slice/1. def slice(_enumerator), do: {:error, __MODULE__} end @error_process "#{inspect __MODULE__} :process field is required" @error_function "#{inspect __MODULE__} :function field must be the name of a #{inspect StructuredIO} public function" @doc """ Builds a new `#{inspect __MODULE__}` for the specified `StructuredIO` `process`, `function`, and `additional_arguments` to that function. ## Examples iex> {:ok, ...> enumerator} = StructuredIO.Enumerator.new(%{process: :a_process, ...> function: :read_across, ...> additional_arguments: ["", ...> ""]}) iex> enumerator %StructuredIO.Enumerator{process: :a_process, function: :read_across, additional_arguments: ["", ""], arguments: [:a_process, "", ""]} iex> StructuredIO.Enumerator.new %{function: :read_across, ...> additional_arguments: ["", ...> ""]} {:error, #{inspect @error_process}} iex> StructuredIO.Enumerator.new %{process: :a_process, ...> additional_arguments: ["", ...> ""]} {:error, #{inspect @error_function}} iex> StructuredIO.Enumerator.new %{process: :a_process, ...> function: "read_across", ...> additional_arguments: ["", ...> ""]} {:error, #{inspect @error_function}} iex> StructuredIO.Enumerator.new %{process: :a_process, ...> function: :not_a_function} {:error, "function StructuredIO.not_a_function/1 is undefined or private"} iex> StructuredIO.Enumerator.new %{process: :a_process, ...> function: :read_across} {:error, "function StructuredIO.read_across/1 is undefined or private"} iex> StructuredIO.Enumerator.new %{process: :a_process, ...> function: :read_across, ...> additional_arguments: "too-few-args"} {:error, "function StructuredIO.read_across/2 is undefined or private"} """ @since "0.6.0" @spec new(%{process: GenServer.server, function: atom, additional_arguments: any}) :: {:ok, t} | StructuredIO.error def new(%{process: nil}=_enumerator), do: {:error, @error_process} def new(%{function: nil}=_enumerator), do: {:error, @error_function} def new(%{process: process, function: function}=enumerator) when is_atom(function) do addl_args = enumerator |> Map.get(:additional_arguments) |> List.wrap function_arity = length(addl_args) + 1 if function_exported?(StructuredIO, function, function_arity) do {:ok, struct(__MODULE__, process: process, function: function, additional_arguments: addl_args, arguments: [process | addl_args])} else {:error, "function #{inspect StructuredIO}.#{function}/#{function_arity} is undefined or private"} end end def new(%{process: _, function: _}=_enumerator), do: {:error, @error_function} def new(%{function: _}=_enumerator), do: {:error, @error_process} def new(%{process: _}=_enumerator), do: {:error, @error_function} @doc """ Sets a timeout for the specified `#{inspect __MODULE__}`. This value is passed in each call to the `StructuredIO.read*` function. ## Examples iex> {:ok, ...> enumerator} = StructuredIO.Enumerator.new(%{process: :a_process, ...> function: :read_across, ...> additional_arguments: ["", ...> ""]}) iex> enumerator %StructuredIO.Enumerator{process: :a_process, function: :read_across, additional_arguments: ["", ""], arguments: [:a_process, "", ""]} iex> enumerator_with_timeout = StructuredIO.Enumerator.timeout(enumerator, ...> 1000) %StructuredIO.Enumerator{process: :a_process, function: :read_across, additional_arguments: ["", ""], arguments: [:a_process, "", "", 1000], timeout: 1000} iex> StructuredIO.Enumerator.timeout enumerator_with_timeout, ...> nil enumerator """ @impl true @since "0.7.0" @spec timeout(t, timeout | nil) :: t def timeout(%__MODULE__{}=enumerator, timeout) do new_arguments = cond do enumerator.timeout && timeout -> List.replace_at enumerator.arguments, -1, timeout timeout -> List.insert_at enumerator.arguments, -1, timeout enumerator.timeout -> List.delete_at enumerator.arguments, -1 :else -> enumerator.arguments end %{enumerator | arguments: new_arguments, timeout: timeout} end end