Files
n8n-openai-adapter/lib/elixir/lib/string_io.ex
T

464 lines
13 KiB
Elixir

defmodule StringIO do
@moduledoc """
Controls an IO device process that wraps a string.
A `StringIO` IO device can be passed as a "device" to
most of the functions in the `IO` module.
## Examples
iex> {:ok, pid} = StringIO.open("foo")
iex> IO.read(pid, 2)
"fo"
"""
# We're implementing the GenServer behaviour instead of using the
# `use GenServer` macro, because we don't want the `child_spec/1`
# function as it doesn't make sense to be started under a supervisor.
@behaviour GenServer
@doc ~S"""
Creates an IO device.
`string` will be the initial input of the newly created
device.
The device will be created and sent to the function given.
When the function returns, the device will be closed. The final
result will be a tuple with `:ok` and the result of the function.
## Options
* `:capture_prompt` - if set to `true`, prompts (specified as
arguments to `IO.get*` functions) are captured in the output.
Defaults to `false`.
* `:encoding` (since v1.10.0) - encoding of the IO device. Allowed
values are `:unicode` (default) and `:latin1`.
## Examples
iex> StringIO.open("foo", [], fn pid ->
...> input = IO.gets(pid, ">")
...> IO.write(pid, "The input was #{input}")
...> StringIO.contents(pid)
...> end)
{:ok, {"", "The input was foo"}}
iex> StringIO.open("foo", [capture_prompt: true], fn pid ->
...> input = IO.gets(pid, ">")
...> IO.write(pid, "The input was #{input}")
...> StringIO.contents(pid)
...> end)
{:ok, {"", ">The input was foo"}}
"""
@doc since: "1.7.0"
@spec open(binary, keyword, (pid -> res)) :: {:ok, res} when res: var
def open(string, options, function)
when is_binary(string) and is_list(options) and is_function(function, 1) do
{:ok, pid} = GenServer.start_link(__MODULE__, {string, options}, [])
try do
{:ok, function.(pid)}
after
{:ok, {_input, _output}} = close(pid)
end
end
@doc ~S"""
Creates an IO device.
`string` will be the initial input of the newly created
device.
`options_or_function` can be a keyword list of options or
a function.
If options are provided, the result will be `{:ok, pid}`, returning the
IO device created. The option `:capture_prompt`, when set to `true`, causes
prompts (which are specified as arguments to `IO.get*` functions) to be
included in the device's output.
If a function is provided, the device will be created and sent to the
function. When the function returns, the device will be closed. The final
result will be a tuple with `:ok` and the result of the function.
## Examples
iex> {:ok, pid} = StringIO.open("foo")
iex> IO.gets(pid, ">")
"foo"
iex> StringIO.contents(pid)
{"", ""}
iex> {:ok, pid} = StringIO.open("foo", capture_prompt: true)
iex> IO.gets(pid, ">")
"foo"
iex> StringIO.contents(pid)
{"", ">"}
iex> StringIO.open("foo", fn pid ->
...> input = IO.gets(pid, ">")
...> IO.write(pid, "The input was #{input}")
...> StringIO.contents(pid)
...> end)
{:ok, {"", "The input was foo"}}
"""
@spec open(binary, keyword) :: {:ok, pid}
@spec open(binary, (pid -> res)) :: {:ok, res} when res: var
def open(string, options_or_function \\ [])
def open(string, options_or_function) when is_binary(string) and is_list(options_or_function) do
GenServer.start_link(__MODULE__, {string, options_or_function}, [])
end
def open(string, options_or_function)
when is_binary(string) and is_function(options_or_function, 1) do
open(string, [], options_or_function)
end
@doc """
Returns the current input/output buffers for the given IO
device.
## Examples
iex> {:ok, pid} = StringIO.open("in")
iex> IO.write(pid, "out")
iex> StringIO.contents(pid)
{"in", "out"}
"""
@spec contents(pid) :: {binary, binary}
def contents(pid) when is_pid(pid) do
GenServer.call(pid, :contents)
end
@doc """
Flushes the output buffer and returns its current contents.
## Examples
iex> {:ok, pid} = StringIO.open("in")
iex> IO.write(pid, "out")
iex> StringIO.flush(pid)
"out"
iex> StringIO.contents(pid)
{"in", ""}
"""
@spec flush(pid) :: binary
def flush(pid) when is_pid(pid) do
GenServer.call(pid, :flush)
end
@doc """
Stops the IO device and returns the remaining input/output
buffers.
## Examples
iex> {:ok, pid} = StringIO.open("in")
iex> IO.write(pid, "out")
iex> StringIO.close(pid)
{:ok, {"in", "out"}}
"""
@spec close(pid) :: {:ok, {binary, binary}}
def close(pid) when is_pid(pid) do
GenServer.call(pid, :close)
end
## callbacks
@impl true
def init({string, options}) do
capture_prompt = options[:capture_prompt] || false
encoding = options[:encoding] || :unicode
{:ok, %{encoding: encoding, input: string, output: "", capture_prompt: capture_prompt}}
end
@impl true
def handle_info({:io_request, from, reply_as, req}, state) do
state = io_request(from, reply_as, req, state)
{:noreply, state}
end
def handle_info(_message, state) do
{:noreply, state}
end
@impl true
def handle_call(:contents, _from, %{input: input, output: output} = state) do
{:reply, {input, output}, state}
end
def handle_call(:flush, _from, %{output: output} = state) do
{:reply, output, %{state | output: ""}}
end
def handle_call(:close, _from, %{input: input, output: output} = state) do
{:stop, :normal, {:ok, {input, output}}, state}
end
defp io_request(from, reply_as, req, state) do
{reply, state} = io_request(req, state)
io_reply(from, reply_as, reply)
state
end
defp io_request({:put_chars, chars} = req, state) do
put_chars(:latin1, chars, req, state)
end
defp io_request({:put_chars, mod, fun, args} = req, state) do
put_chars(:latin1, apply(mod, fun, args), req, state)
end
defp io_request({:put_chars, encoding, chars} = req, state) do
put_chars(encoding, chars, req, state)
end
defp io_request({:put_chars, encoding, mod, fun, args} = req, state) do
put_chars(encoding, apply(mod, fun, args), req, state)
end
defp io_request({:get_chars, prompt, count}, state) when count >= 0 do
io_request({:get_chars, :latin1, prompt, count}, state)
end
defp io_request({:get_chars, encoding, prompt, count}, state) when count >= 0 do
get_chars(encoding, prompt, count, state)
end
defp io_request({:get_line, prompt}, state) do
io_request({:get_line, :latin1, prompt}, state)
end
defp io_request({:get_line, encoding, prompt}, state) do
get_line(encoding, prompt, state)
end
defp io_request({:get_until, prompt, mod, fun, args}, state) do
io_request({:get_until, :latin1, prompt, mod, fun, args}, state)
end
defp io_request({:get_until, encoding, prompt, mod, fun, args}, state) do
get_until(encoding, prompt, mod, fun, args, state)
end
defp io_request({:get_password, encoding}, state) do
get_line(encoding, "", state)
end
defp io_request({:setopts, [encoding: encoding]}, state) when encoding in [:latin1, :unicode] do
{:ok, %{state | encoding: encoding}}
end
defp io_request({:setopts, _opts}, state) do
{{:error, :enotsup}, state}
end
defp io_request(:getopts, state) do
{[binary: true, encoding: state.encoding], state}
end
defp io_request({:get_geometry, :columns}, state) do
{{:error, :enotsup}, state}
end
defp io_request({:get_geometry, :rows}, state) do
{{:error, :enotsup}, state}
end
defp io_request({:requests, reqs}, state) do
io_requests(reqs, {:ok, state})
end
defp io_request(_, state) do
{{:error, :request}, state}
end
## put_chars
defp put_chars(encoding, chars, req, state) do
case :unicode.characters_to_binary(chars, encoding, state.encoding) do
string when is_binary(string) ->
{:ok, %{state | output: state.output <> string}}
{_, _, _} ->
{{:error, {:no_translation, encoding, state.encoding}}, state}
end
rescue
ArgumentError -> {{:error, req}, state}
end
## get_chars
defp get_chars(encoding, prompt, count, %{input: input} = state) do
case get_chars(input, encoding, count) do
{:error, _} = error ->
{error, state}
{result, input} ->
{result, state_after_read(state, input, prompt, 1)}
end
end
defp get_chars("", _encoding, _count) do
{:eof, ""}
end
defp get_chars(input, :latin1, count) when byte_size(input) < count do
{input, ""}
end
defp get_chars(input, :latin1, count) do
<<chars::binary-size(count), rest::binary>> = input
{chars, rest}
end
defp get_chars(input, :unicode, count) do
with {:ok, count} <- split_at(input, count, 0) do
<<chars::binary-size(count), rest::binary>> = input
{chars, rest}
end
end
defp split_at(_, 0, acc),
do: {:ok, acc}
defp split_at(<<h::utf8, t::binary>>, count, acc),
do: split_at(t, count - 1, acc + byte_size(<<h::utf8>>))
defp split_at(<<_, _::binary>>, _count, _acc),
do: {:error, :invalid_unicode}
defp split_at(<<>>, _count, acc),
do: {:ok, acc}
## get_line
defp get_line(encoding, prompt, %{input: input} = state) do
case bytes_until_eol(input, encoding, 0) do
{:split, 0} ->
{:eof, state_after_read(state, "", prompt, 1)}
{:split, count} ->
{result, remainder} = :erlang.split_binary(input, count)
{result, state_after_read(state, remainder, prompt, 1)}
{:replace_split, count} ->
{result, remainder} = :erlang.split_binary(input, count)
result = binary_part(result, 0, byte_size(result) - 2) <> "\n"
{result, state_after_read(state, remainder, prompt, 1)}
:error ->
{{:error, :collect_line}, state}
end
end
## get_until
defp get_until(encoding, prompt, mod, fun, args, %{input: input} = state) do
case get_until(input, encoding, mod, fun, args, [], 0) do
{result, input, count} ->
input =
case input do
:eof -> ""
_ -> list_to_binary(input, encoding)
end
{get_until_result(result, encoding), state_after_read(state, input, prompt, count)}
:error ->
{:error, state}
end
end
defp get_until("", encoding, mod, fun, args, continuation, count) do
case apply(mod, fun, [continuation, :eof | args]) do
{:done, result, rest} ->
{result, rest, count + 1}
{:more, next_continuation} ->
get_until("", encoding, mod, fun, args, next_continuation, count + 1)
end
end
defp get_until(chars, encoding, mod, fun, args, continuation, count) do
case bytes_until_eol(chars, encoding, 0) do
{kind, size} when kind in [:split, :replace_split] ->
{line, rest} = :erlang.split_binary(chars, size)
case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do
{:done, result, :eof} ->
{result, rest, count + 1}
{:done, result, extra} ->
{result, extra ++ binary_to_list(rest, encoding), count + 1}
{:more, next_continuation} ->
get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
end
:error ->
:error
end
end
defp binary_to_list(data, :unicode) when is_binary(data), do: String.to_charlist(data)
defp binary_to_list(data, :latin1) when is_binary(data), do: :erlang.binary_to_list(data)
defp list_to_binary(data, _) when is_binary(data), do: data
defp list_to_binary(data, :unicode) when is_list(data), do: List.to_string(data)
defp list_to_binary(data, :latin1) when is_list(data), do: :erlang.list_to_binary(data)
# From https://www.erlang.org/doc/apps/stdlib/io_protocol.html: result can be any
# Erlang term, but if it is a list(), the I/O server can convert it to a binary().
defp get_until_result(data, encoding) when is_list(data), do: list_to_binary(data, encoding)
defp get_until_result(data, _), do: data
## io_requests
defp io_requests([req | rest], {:ok, state}) do
io_requests(rest, io_request(req, state))
end
defp io_requests(_, result) do
result
end
## helpers
defp state_after_read(%{capture_prompt: false} = state, remainder, _prompt, _count) do
%{state | input: remainder}
end
defp state_after_read(%{capture_prompt: true, output: output} = state, remainder, prompt, count) do
output = <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>
%{state | input: remainder, output: output}
end
defp bytes_until_eol("", _, count), do: {:split, count}
defp bytes_until_eol(<<"\r\n"::binary, _::binary>>, _, count), do: {:replace_split, count + 2}
defp bytes_until_eol(<<"\n"::binary, _::binary>>, _, count), do: {:split, count + 1}
defp bytes_until_eol(<<head::utf8, tail::binary>>, :unicode, count) do
bytes_until_eol(tail, :unicode, count + byte_size(<<head::utf8>>))
end
defp bytes_until_eol(<<_, tail::binary>>, :latin1, count) do
bytes_until_eol(tail, :latin1, count + 1)
end
defp bytes_until_eol(<<_::binary>>, _, _), do: :error
defp io_reply(from, reply_as, reply) do
send(from, {:io_reply, reply_as, reply})
end
end