defmodule StructuredIO do @moduledoc """ A process for performing I/O of structured data, such as markup or binary-encoded data. ## Encoding The process operates in either **binary mode** or **Unicode mode** (see `#{inspect __MODULE__}.start/2` and `#{inspect __MODULE__}.start_link/2`). When in binary mode, the result of a read operation is a binary, regardless of whether the data read is `String.valid?/1`. In Unicode mode, the result of a read operation is an `t:error/0` if the data read is not properly encoded Unicode data. """ defmodule State do @moduledoc false defstruct data: [], mode: nil @typedoc false @type t :: %__MODULE__{data: iodata | IO.chardata | String.Chars.t, mode: nil | StructuredIO.mode} end use GenServer alias StructuredIO.{Collector,Deprecated,Enumerator,Scanner} @typedoc """ A number of bytes or graphemes in a measured data element. """ @type count :: Scanner.count @typedoc """ An error result. """ @type error :: {:error, atom | binary} @typedoc """ A binary value which marks the beginning of an enclosed data element. """ @type left :: Scanner.left @typedoc """ The portion of a binary value matched in a read operation. """ @type match :: Scanner.match @typedoc """ A mode of operation for the process: either binary or Unicode. """ @type mode :: :binary | :unicode @valid_modes [:binary, :unicode] @typedoc """ A binary value which marks the end of an enclosed or terminated data element. """ @type right :: Scanner.right @doc false @deprecated "Call #{inspect __MODULE__}.read_across in binary mode instead" defdelegate binread_across(structured_io, left, right, timeout \\ 5000), to: Deprecated @doc false @deprecated "Call #{inspect __MODULE__}.read_between in binary mode instead" defdelegate binread_between(structured_io, left, right, timeout \\ 5000), to: Deprecated @doc false @deprecated "Call #{inspect __MODULE__}.read_through in binary mode instead" defdelegate binread_through(structured_io, right, timeout \\ 5000), to: Deprecated @doc false @deprecated "Call #{inspect __MODULE__}.read_to in binary mode instead" defdelegate binread_to(structured_io, to, timeout \\ 5000), to: Deprecated @doc false @deprecated "Call #{inspect __MODULE__}.write in binary mode instead" defdelegate binwrite(structured_io, iodata), to: Deprecated @doc """ Returns a value that can be passed to `Enum.into/2` or `Enum.into/3` for writing data to the specified `structured_io`. iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.read_between structured_io, ...> "", ...> "" "" iex> collector = StructuredIO.collect(structured_io) iex> ["foo", ...> "bar", ...> "baz"] iex> |> Enum.into(collector) iex> StructuredIO.read_between structured_io, ...> "", ...> "" "foo" iex> StructuredIO.read_between structured_io, ...> "", ...> "" "bar" iex> StructuredIO.read_between structured_io, ...> "", ...> "" "baz" iex> StructuredIO.read_between structured_io, ...> "", ...> "" "" """ @since "0.6.0" @spec collect(GenServer.server) :: Collector.t def collect(structured_io) do {:ok, collector} = Collector.new(%{process: structured_io, function: :write}) collector end @doc """ Returns a value that can be passed to functions such as `Enum.map/2` for reading data elements from the specified `structured_io`, using the specified `#{inspect __MODULE__}` `function`, and the specified `left` and/or `right`. Note that enumeration is not a purely functional operation; it consumes data elements from the underlying `#{inspect __MODULE__}` process. 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> structured_io ...> |> StructuredIO.enumerate_with(:read_between, ...> "", ...> "") ...> |> Enum.map(&String.upcase/1) ["FOO", "BAR", "BAZ"] iex> StructuredIO.read_between structured_io, ...> "", ...> "" "" 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> structured_io ...> |> StructuredIO.enumerate_with(:read_through, ...> "
") ...> |> Enum.map(&String.upcase/1) ["FOO
", "BAR
", "BAZ
"] iex> StructuredIO.read_through structured_io, ...> "
" "" """ @since "0.6.0" @spec enumerate_with(GenServer.server, :read_across | :read_across_ignoring_overlap | :read_between | :read_between_ignoring_overlap, left, right) :: Enumerator.t def enumerate_with(structured_io, function, left, right) when function in ~w{read_across read_across_ignoring_overlap read_between read_between_ignoring_overlap}a do {:ok, enumerator} = Enumerator.new(%{process: structured_io, function: function, additional_arguments: [left, right]}) enumerator end @since "0.6.0" @spec enumerate_with(GenServer.server, :read_through | :read_to, right) :: Enumerator.t def enumerate_with(structured_io, function, right) when function in [:read_through, :read_to] do {:ok, enumerator} = Enumerator.new(%{process: structured_io, function: function, additional_arguments: [right]}) enumerator end @doc """ Gets the mode of the specified `structured_io`. ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.mode structured_io :binary iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.mode structured_io :unicode """ @since "0.5.0" @spec mode(GenServer.server) :: mode def mode(structured_io) do request = :mode GenServer.call structured_io, request end @doc """ Reads data from the specified `structured_io` in the specified quantity. In binary mode, `count` is a number of bytes; in Unicode mode, `count` is a number of graphemes. If the data in the process does not contain at least the expected quantity of data, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<23, 45>> :ok iex> StructuredIO.read structured_io, ...> 3 "" iex> StructuredIO.write structured_io, ...> <<67>> :ok iex> StructuredIO.read structured_io, ...> 3 <<23, 45, 67>> iex> StructuredIO.read structured_io, ...> 3 "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "\\r\\nfo" :ok iex> StructuredIO.read structured_io, ...> 4 "" iex> StructuredIO.write structured_io, ...> "o" :ok iex> StructuredIO.read structured_io, ...> 4 "\\r\\nfoo" iex> StructuredIO.read structured_io, ...> 4 "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read structured_io, ...> 4 "" iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.read structured_io, ...> 4 "πŸ˜•" iex> StructuredIO.read structured_io, ...> 4 "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read structured_io, ...> 1 {:error, "UnicodeConversionError: incomplete encoding starting at \#{inspect fragment1}"} iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.read structured_io, ...> 1 "πŸ˜•" iex> StructuredIO.read structured_io, ...> 1 "" See `#{inspect __MODULE__}.mode/1`. """ @since "0.8.0" @spec read(GenServer.server, count) :: match | error @since "0.8.0" @spec read(GenServer.server, count, timeout) :: match | error def read(structured_io, count, timeout \\ 5000) do request = {:read, count} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc """ Reads data from the specified `structured_io` beginning with the specified `left` and ending with the occurrence of the specified `right` that corresponds to it, inclusive. If the data in the process does not both begin with `left` and contain a corresponding `right`, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<0, 0, 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255, 255, 255, 255>> :ok iex> StructuredIO.read_across structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> "" iex> StructuredIO.write structured_io, ...> <<255, 0, 0, 0, 7, 8, 9, 255, 255, 255>> :ok iex> StructuredIO.read_across structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> <<0, 0, 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255, 255, 255, 255, 255>> iex> StructuredIO.read_across structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> <<0, 0, 0, 7, 8, 9, 255, 255, 255>> iex> StructuredIO.read_across structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foobar StructuredIO.read_across structured_io, ...> "", ...> "" "" iex> StructuredIO.write structured_io, ...> ">baz" :ok iex> StructuredIO.read_across structured_io, ...> "", ...> "" "foobar" iex> StructuredIO.read_across structured_io, ...> "", ...> "" "baz" iex> StructuredIO.read_across structured_io, ...> "", ...> "" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> "" :ok iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_across structured_io, ...> "", ...> "" "" iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "" :ok iex> StructuredIO.read_across structured_io, ...> "", ...> "" "πŸ˜•" iex> StructuredIO.read_across structured_io, ...> "", ...> "" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "" :ok iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_across structured_io, ...> "", ...> "" {:error, "UnicodeConversionError: incomplete encoding starting at \#{inspect fragment1}"} iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "" :ok iex> StructuredIO.read_across structured_io, ...> "", ...> "" "πŸ˜•" iex> StructuredIO.read_across structured_io, ...> "", ...> "" "" """ @since "0.1.0" @spec read_across(GenServer.server, left, right) :: match | error @since "0.2.0" @spec read_across(GenServer.server, left, right, timeout) :: match | error def read_across(structured_io, left, right, timeout \\ 5000) do request = {:read_across, left, right} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc """ Reads data from the specified `structured_io` beginning with the specified `left` and ending with the first occurrence of the specified `right`, inclusive. If the data in the process does not both begin with `left` and contain `right`, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<0, 0, 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255>> :ok iex> StructuredIO.read_across_ignoring_overlap structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> "" iex> StructuredIO.write structured_io, ...> <<255>> :ok iex> StructuredIO.read_across_ignoring_overlap structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> <<0, 0, 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255, 255>> iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foobar StructuredIO.read_across_ignoring_overlap structured_io, ...> "", ...> "" "" iex> StructuredIO.write structured_io, ...> ">" :ok iex> StructuredIO.read_across_ignoring_overlap structured_io, ...> "", ...> "" "foobar" """ @since "0.7.0" @spec read_across_ignoring_overlap(GenServer.server, left, right) :: match | error @since "0.7.0" @spec read_across_ignoring_overlap(GenServer.server, left, right, timeout) :: match | error def read_across_ignoring_overlap(structured_io, left, right, timeout \\ 5000) do request = {:read_across_ignoring_overlap, left, right} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc """ Reads data from the specified `structured_io` beginning with the specified `left` and ending with the occurrence of the specified `right` that corresponds to it, exclusive. If the data in the process does not both begin with `left` and contain a corresponding `right`, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<0, 0, 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255, 255, 255, 255>> :ok iex> StructuredIO.read_between structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> "" iex> StructuredIO.write structured_io, ...> <<255>> :ok iex> StructuredIO.read_between structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> <<1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255, 255>> iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foobar StructuredIO.read_between structured_io, ...> "", ...> "" "" iex> StructuredIO.write structured_io, ...> ">" :ok iex> StructuredIO.read_between structured_io, ...> "", ...> "" "foobar" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> "" :ok iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_between structured_io, ...> "", ...> "" "" iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "" :ok iex> StructuredIO.read_between structured_io, ...> "", ...> "" "πŸ˜•" iex> StructuredIO.read_between structured_io, ...> "", ...> "" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "" :ok iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_between structured_io, ...> "", ...> "" {:error, "UnicodeConversionError: incomplete encoding starting at \#{inspect fragment1}"} iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "" :ok iex> StructuredIO.read_between structured_io, ...> "", ...> "" "πŸ˜•" iex> StructuredIO.read_between structured_io, ...> "", ...> "" "" """ @since "0.4.0" @spec read_between(GenServer.server, left, right) :: match | error @since "0.2.0" @spec read_between(GenServer.server, left, right, timeout) :: match | error def read_between(structured_io, left, right, timeout \\ 5000) do request = {:read_between, left, right} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc """ Reads data from the specified `structured_io` beginning with the specified `left` and ending with the first occurrence of the specified `right`, exclusive. If the data in the process does not both begin with `left` and contain `right`, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<0, 0, 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, 255, 255>> :ok iex> StructuredIO.read_between_ignoring_overlap structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> "" iex> StructuredIO.write structured_io, ...> <<255>> :ok iex> StructuredIO.read_between_ignoring_overlap structured_io, ...> <<0, 0, 0>>, ...> <<255, 255, 255>> <<1, 2, 3, 0, 0, 0, 4, 5, 6>> iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foobar StructuredIO.read_between_ignoring_overlap structured_io, ...> "", ...> "" "" iex> StructuredIO.write structured_io, ...> ">" :ok iex> StructuredIO.read_between_ignoring_overlap structured_io, ...> "", ...> "" "foobar" """ @since "0.7.0" @spec read_between_ignoring_overlap(GenServer.server, left, right) :: match | error @since "0.7.0" @spec read_between_ignoring_overlap(GenServer.server, left, right, timeout) :: match | error def read_between_ignoring_overlap(structured_io, left, right, timeout \\ 5000) do request = {:read_between_ignoring_overlap, left, right} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc """ Reads data from the specified `structured_io` if and until the specified `right` is encountered, including `right`. If `right` is not encountered, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<1, 2, 3, 255, 255>> :ok iex> StructuredIO.read_through structured_io, ...> <<255, 255, 255>> "" iex> StructuredIO.write structured_io, ...> <<255, 4, 5, 6, 255, 255, 255>> :ok iex> StructuredIO.read_through structured_io, ...> <<255, 255, 255>> <<1, 2, 3, 255, 255, 255>> iex> StructuredIO.read_through structured_io, ...> <<255, 255, 255>> <<4, 5, 6, 255, 255, 255>> iex> StructuredIO.read_through structured_io, ...> <<255, 255, 255>> "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foo
StructuredIO.read_through structured_io, ...> "
" "" iex> StructuredIO.write structured_io, ...> ">bar
" :ok iex> StructuredIO.read_through structured_io, ...> "
" "foo
" iex> StructuredIO.read_through structured_io, ...> "
" "bar
" iex> StructuredIO.read_through structured_io, ...> "
" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_through structured_io, ...> "
" "" iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "
" :ok iex> StructuredIO.read_through structured_io, ...> "
" "πŸ˜•
" iex> StructuredIO.read_through structured_io, ...> "
" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_through structured_io, ...> "
" {:error, "UnicodeConversionError: incomplete encoding starting at \#{inspect fragment1}"} iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "
" :ok iex> StructuredIO.read_through structured_io, ...> "
" "πŸ˜•
" iex> StructuredIO.read_through structured_io, ...> "
" "" """ @since "0.2.0" @spec read_through(GenServer.server, right) :: match | error @since "0.2.0" @spec read_through(GenServer.server, right, timeout) :: match | error def read_through(structured_io, right, timeout \\ 5000) do request = {:read_through, right} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc """ Reads data from the specified `structured_io` if and until the specified `right` is encountered, excluding `right`. If `right` is not encountered, the result is an empty binary (`""`). ## Examples iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> StructuredIO.write structured_io, ...> <<1, 2, 3, 255, 255>> :ok iex> StructuredIO.read_to structured_io, ...> <<255, 255, 255>> "" iex> StructuredIO.write structured_io, ...> <<255, 4, 5, 6, 255, 255, 255>> :ok iex> StructuredIO.read_to structured_io, ...> <<255, 255, 255>> <<1, 2, 3>> iex> StructuredIO.read_through structured_io, ...> <<255, 255, 255>> <<255, 255, 255>> iex> StructuredIO.read_to structured_io, ...> <<255, 255, 255>> <<4, 5, 6>> iex> StructuredIO.read_to structured_io, ...> <<255, 255, 255>> "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> StructuredIO.write structured_io, ...> "foo
StructuredIO.read_to structured_io, ...> "
" "" iex> StructuredIO.write structured_io, ...> ">bar
" :ok iex> StructuredIO.read_to structured_io, ...> "
" "foo" iex> StructuredIO.read_through structured_io, ...> "
" "
" iex> StructuredIO.read_to structured_io, ...> "
" "bar" iex> StructuredIO.read_to structured_io, ...> "
" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:binary) iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_to structured_io, ...> "
" "" iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "
" :ok iex> StructuredIO.read_to structured_io, ...> "
" "πŸ˜•" iex> StructuredIO.read_to structured_io, ...> "
" "" iex> {:ok, ...> structured_io} = StructuredIO.start_link(:unicode) iex> <> = "πŸ˜•" iex> StructuredIO.write structured_io, ...> fragment1 :ok iex> StructuredIO.read_to structured_io, ...> "
" {:error, "UnicodeConversionError: incomplete encoding starting at \#{inspect fragment1}"} iex> StructuredIO.write structured_io, ...> fragment2 :ok iex> StructuredIO.write structured_io, ...> "
" :ok iex> StructuredIO.read_to structured_io, ...> "
" "πŸ˜•" iex> StructuredIO.read_to structured_io, ...> "
" "" """ @since "0.2.0" @spec read_to(GenServer.server, right) :: match | error @since "0.2.0" @spec read_to(GenServer.server, right, timeout) :: match | error def read_to(structured_io, right, timeout \\ 5000) do request = {:read_to, right} structured_io |> GenServer.call(request, timeout) |> convert_if_error end @doc false @deprecated "Call #{inspect __MODULE__}.start/1 instead" defdelegate start, to: Deprecated @doc """ Starts a `#{inspect __MODULE__}` process without links (outside a supervision tree) with the specified `mode` and `options`. ## Examples iex> StructuredIO.start :super_pursuit_mode {:error, "invalid mode :super_pursuit_mode"} See `#{inspect __MODULE__}.start_link/2`. """ @since "0.5.0" @spec start(mode) :: GenServer.on_start @since "0.5.0" @spec start(mode, GenServer.options) :: GenServer.on_start def start(mode, options \\ []) do with {:ok, mode} <- compute_mode(mode, :start) do GenServer.start __MODULE__, %State{mode: mode}, options end end @doc false @deprecated "Call #{inspect __MODULE__}.start_link/1 instead" defdelegate start_link, to: Deprecated @doc """ Starts a `#{inspect __MODULE__}` process linked to the current process with the specified `mode` and `options`. ## Examples iex> StructuredIO.start_link :super_pursuit_mode {:error, "invalid mode :super_pursuit_mode"} See `#{inspect __MODULE__}.mode/1`, `#{inspect __MODULE__}.read_across/3`, `#{inspect __MODULE__}.read_between/3`, `#{inspect __MODULE__}.read_through/2`, and `#{inspect __MODULE__}.read_to/2` for more examples. """ @since "0.5.0" @spec start_link(mode) :: GenServer.on_start @since "0.5.0" @spec start_link(mode, GenServer.options) :: GenServer.on_start def start_link(mode, options \\ []) do with {:ok, mode} <- compute_mode(mode, :start_link) do GenServer.start_link __MODULE__, %State{mode: mode}, options end end @doc """ Stops the specified `structured_io` process. """ @since "0.1.0" @spec stop(GenServer.server) :: :ok @since "0.1.0" @spec stop(GenServer.server, term) :: :ok @since "0.1.0" @spec stop(GenServer.server, term, timeout) :: :ok def stop(structured_io, reason \\ :normal, timeout \\ :infinity) do GenServer.stop structured_io, reason, timeout end @doc """ Writes the specified `data` to the specified `structured_io`. No timeout is available because the operation is performed asynchronously. See `#{inspect __MODULE__}.read_across/3`, `#{inspect __MODULE__}.read_between/3`, `#{inspect __MODULE__}.read_through/2`, and `#{inspect __MODULE__}.read_to/2` for examples. """ @since "0.1.0" @spec write(GenServer.server, iodata | IO.chardata | String.Chars.t) :: :ok | error def write(structured_io, data) do request = {:deprecated_write, data} GenServer.cast structured_io, request end # Callbacks def handle_call(:mode, _from, %{mode: mode}=state), do: {:reply, mode, state} def handle_call({:read, count}, _from, state) do unit = scan_unit(state) scan(state, :scan, [unit, count]) end def handle_call({:read_across, left, right}, _from, state) do scan(state, :scan_across, [left, right]) end def handle_call({:read_across_ignoring_overlap, left, right}, _from, state) do scan(state, :scan_across_ignoring_overlap, [left, right]) end def handle_call({:read_between, left, right}, _from, state) do scan(state, :scan_between, [left, right]) end def handle_call({:read_between_ignoring_overlap, left, right}, _from, state) do scan(state, :scan_between_ignoring_overlap, [left, right]) end def handle_call({:read_through, right}, _from, state) do scan(state, :scan_through, [right]) end def handle_call({:read_to, right}, _from, state) do scan(state, :scan_to, [right]) end @doc false defdelegate handle_call(request, from, state), to: Deprecated # # TODO: Add a handler for the :write message when the :deprecated_write message is eliminated # def handle_cast({:write, new_data}, %{data: data}=state) do # new_state = %{state | data: [data, new_data]} # {:noreply, new_state} # end @doc false defdelegate handle_cast(request, state), to: Deprecated @doc false def init(args), do: {:ok, args} @spec binary_data(State.t) :: {:ok, binary} | error # # TODO: Don’t handle `nil` :mode when deprecated `.start*` usage is eliminated defp binary_data(%{data: iodata, mode: nil}) do {:ok, IO.iodata_to_binary(iodata)} end defp binary_data(%{data: iodata, mode: :binary}) do {:ok, IO.iodata_to_binary(iodata)} end defp binary_data(%{data: chardata, mode: :unicode}) do try do IO.chardata_to_string chardata rescue e in UnicodeConversionError -> {:error, e} else string -> {:ok, string} end end @spec compute_mode(mode, atom) :: {:ok, mode} | error defp compute_mode(mode, _) when mode in @valid_modes, do: {:ok, mode} defp compute_mode(mode, _), do: {:error, "invalid mode #{inspect mode}"} @spec convert_if_error(any) :: error | any defp convert_if_error({:error, error}) do if Exception.exception?(error) do type = error |> Map.fetch!(:__struct__) |> inspect {:error, "#{type}: #{error.message}"} else if is_atom(error) do {:error, error} else {:error, to_string(error)} end end end defp convert_if_error(other), do: other @spec read_reply(nil | {Scanner.match, Scanner.remainder}, State.t) :: {:reply, Scanner.match, State.t} defp read_reply(nil, state), do: {:reply, "", state} defp read_reply({match, remainder}, state) do new_state = %{state | data: remainder} {:reply, match, new_state} end @spec scan(State.t, atom, [any]) :: {:reply, Scanner.match | error, State.t} defp scan(state, function, arguments) do case binary_data(state) do {:error, _}=error -> {:reply, error, state} {:ok, binary} -> Scanner |> apply(function, [binary | arguments]) |> read_reply(state) end end @spec scan_unit(State.t) :: Scanner.unit defp scan_unit(%{mode: :binary}=_state), do: :bytes defp scan_unit(%{mode: :unicode}=_state), do: :graphemes end