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

389 lines
9.7 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"
"""
use GenServer
@doc """
Creates an IO device.
`string` will be the initial input of the newly created
device.
If the `:capture_prompt` option is set to `true`,
prompts (specified as arguments to `IO.get*` functions)
are captured.
## 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)
{"", ">"}
"""
@spec open(binary, keyword) :: {:ok, pid}
def open(string, options \\ []) when is_binary(string) do
GenServer.start_link(__MODULE__, {string, options}, [])
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
def init({string, options}) do
capture_prompt = options[:capture_prompt] || false
{:ok, %{input: string, output: "", capture_prompt: capture_prompt}}
end
def handle_info({:io_request, from, reply_as, req}, s) do
s = io_request(from, reply_as, req, s)
{:noreply, s}
end
def handle_info(msg, s) do
super(msg, s)
end
def handle_call(:contents, _from, %{input: input, output: output} = s) do
{:reply, {input, output}, s}
end
def handle_call(:flush, _from, %{output: output} = s) do
{:reply, output, %{s | output: ""}}
end
def handle_call(:close, _from, %{input: input, output: output} = s) do
{:stop, :normal, {:ok, {input, output}}, s}
end
def handle_call(request, from, s) do
super(request, from, s)
end
defp io_request(from, reply_as, req, s) do
{reply, s} = io_request(req, s)
io_reply(from, reply_as, to_reply(reply))
s
end
defp io_request({:put_chars, chars} = req, s) do
put_chars(:latin1, chars, req, s)
end
defp io_request({:put_chars, m, f, as} = req, s) do
put_chars(:latin1, apply(m, f, as), req, s)
end
defp io_request({:put_chars, encoding, chars} = req, s) do
put_chars(encoding, chars, req, s)
end
defp io_request({:put_chars, encoding, mod, func, args} = req, s) do
put_chars(encoding, apply(mod, func, args), req, s)
end
defp io_request({:get_chars, prompt, n}, s) when n >= 0 do
io_request({:get_chars, :latin1, prompt, n}, s)
end
defp io_request({:get_chars, encoding, prompt, n}, s) when n >= 0 do
get_chars(encoding, prompt, n, s)
end
defp io_request({:get_line, prompt}, s) do
io_request({:get_line, :latin1, prompt}, s)
end
defp io_request({:get_line, encoding, prompt}, s) do
get_line(encoding, prompt, s)
end
defp io_request({:get_until, prompt, mod, fun, args}, s) do
io_request({:get_until, :latin1, prompt, mod, fun, args}, s)
end
defp io_request({:get_until, encoding, prompt, mod, fun, args}, s) do
get_until(encoding, prompt, mod, fun, args, s)
end
defp io_request({:get_password, encoding}, s) do
get_line(encoding, "", s)
end
defp io_request({:setopts, _opts}, s) do
{{:error, :enotsup}, s}
end
defp io_request(:getopts, s) do
{{:ok, [binary: true, encoding: :unicode]}, s}
end
defp io_request({:get_geometry, :columns}, s) do
{{:error, :enotsup}, s}
end
defp io_request({:get_geometry, :rows}, s) do
{{:error, :enotsup}, s}
end
defp io_request({:requests, reqs}, s) do
io_requests(reqs, {:ok, s})
end
defp io_request(_, s) do
{{:error, :request}, s}
end
## put_chars
defp put_chars(encoding, chars, req, %{output: output} = s) do
case :unicode.characters_to_binary(chars, encoding, :unicode) do
string when is_binary(string) ->
{:ok, %{s | output: output <> string}}
{_, _, _} ->
{{:error, req}, s}
end
end
## get_chars
defp get_chars(encoding, prompt, n, %{input: input} = s) do
case do_get_chars(input, encoding, n) do
{:error, _} = error ->
{error, s}
{result, input} ->
{result, state_after_read(s, input, prompt)}
end
end
defp do_get_chars("", _encoding, _n) do
{:eof, ""}
end
defp do_get_chars(input, :latin1, n) when byte_size(input) < n do
{input, ""}
end
defp do_get_chars(input, :latin1, n) do
<<chars::binary-size(n), rest::binary>> = input
{chars, rest}
end
defp do_get_chars(input, encoding, n) do
try do
case :file_io_server.count_and_find(input, n, encoding) do
{buf_count, split_pos} when buf_count < n or split_pos == :none ->
{input, ""}
{_buf_count, split_pos} ->
<<chars::binary-size(split_pos), rest::binary>> = input
{chars, rest}
end
catch
:exit, :invalid_unicode ->
{:error, :invalid_unicode}
end
end
## get_line
defp get_line(encoding, prompt, %{input: input} = s) do
case bytes_until_eol(input, encoding, 0) do
{:split, 0} ->
{:eof, state_after_read(s, "", prompt)}
{:split, count} ->
{result, remainder} = :erlang.split_binary(input, count)
{result, state_after_read(s, remainder, prompt)}
{:replace_split, count} ->
{result, remainder} = :erlang.split_binary(input, count)
{binary_part(result, 0, byte_size(result) - 2) <> "\n", state_after_read(s, remainder, prompt)}
:error
-> {{:error, :collect_line}, s}
end
end
## get_until
defp get_until(encoding, prompt, mod, fun, args,
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
case :unicode.characters_to_list(input, encoding) do
{:error, _, _} ->
{:error, s}
{:incomplete, _, _} ->
{:error, s}
chars ->
{result, input, count} = do_get_until(chars, encoding, mod, fun, args)
input =
case input do
:eof -> ""
_ -> :unicode.characters_to_binary(input, encoding)
end
s =
if capture_prompt do
%{s | output: <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>}
else
s
end
{result, %{s | input: input}}
end
end
defp do_get_until(chars, encoding, mod, fun, args, continuation \\ [], count \\ 0)
defp do_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} ->
do_get_until('', encoding, mod, fun, args, next_continuation, count + 1)
end
end
defp do_get_until(chars, encoding, mod, fun, args, continuation, count) do
{line, rest} = collect_line(chars)
case apply(mod, fun, [continuation, line | args]) do
{:done, result, :eof} ->
{result, rest, count + 1}
{:done, result, extra} ->
{result, extra ++ rest, count + 1}
{:more, next_continuation} ->
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
end
end
## io_requests
defp io_requests([r | rs], {:ok, s}) do
io_requests(rs, io_request(r, s))
end
defp io_requests(_, result) do
result
end
## helpers
defp state_after_read(%{capture_prompt: false} = s, remainder, _prompt) do
%{s | input: remainder}
end
defp state_after_read(%{capture_prompt: true, output: output} = s, remainder, prompt) do
%{s | input: remainder, output: <<output::binary, IO.chardata_to_string(prompt)::binary>>}
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 collect_line(chars) do
collect_line(chars, [])
end
defp collect_line([], stack) do
{:lists.reverse(stack), []}
end
defp collect_line([?\r, ?\n | rest], stack) do
{:lists.reverse([?\n | stack]), rest}
end
defp collect_line([?\n | rest], stack) do
{:lists.reverse([?\n | stack]), rest}
end
defp collect_line([h | t], stack) do
collect_line(t, [h | stack])
end
defp io_reply(from, reply_as, reply) do
send from, {:io_reply, reply_as, reply}
end
defp to_reply(list) when is_list(list), do: IO.chardata_to_string(list)
defp to_reply(other), do: other
end