Current section

Files

Jump to
opencode_sdk lib opencode client.ex
Raw

lib/opencode/client.ex

defmodule OpenCode.Client do
@moduledoc """
HTTP client used by generated operations.
"""
@default_base_url "http://127.0.0.1:4096"
@spec request(map()) :: {:ok, any()} | {:error, any()}
def request(%{url: url, method: method} = params) do
opts = params[:opts] || []
base = Keyword.get(opts, :base_url, @default_base_url)
time = Keyword.get(opts, :timeout, :infinity)
headers = Keyword.get(opts, :headers, [])
dir = Keyword.get(opts, :directory)
req =
Req.new(url: base <> url, method: method, receive_timeout: time)
|> add_headers(headers)
|> add_directory(dir)
|> add_body(params[:body])
|> add_query(params[:query])
case sse?(params) do
true ->
req = Req.Request.put_header(req, "accept", "text/event-stream")
{:ok, %{stream: sse_stream(req)}}
false ->
case Req.request(req) do
{:ok, %{status: status, body: body}} when status in 200..299 ->
{:ok, body}
{:ok, %{status: status, body: body}} ->
{:error, {status, body}}
{:error, error} ->
{:error, error}
end
end
end
defp add_body(req, nil), do: req
defp add_body(req, body), do: Req.merge(req, json: body)
defp add_query(req, nil), do: req
defp add_query(req, query), do: Req.merge(req, params: query)
defp add_headers(req, []), do: req
defp add_headers(req, headers) when is_map(headers) do
Enum.reduce(headers, req, fn {key, val}, acc ->
Req.Request.put_header(acc, to_string(key), to_string(val))
end)
end
defp add_headers(req, headers) do
Enum.reduce(headers, req, fn {key, val}, acc ->
Req.Request.put_header(acc, to_string(key), to_string(val))
end)
end
defp add_directory(req, nil), do: req
defp add_directory(req, ""), do: req
defp add_directory(req, dir) do
val =
case String.match?(dir, ~r/[^\x00-\x7F]/) do
true -> URI.encode(dir)
false -> dir
end
Req.Request.put_header(req, "x-opencode-directory", val)
end
defp sse?(%{url: url, method: :get}) when url in ["/event", "/global/event"], do: true
defp sse?(_), do: false
defp sse_stream(req) do
Stream.resource(
fn ->
ref = make_ref()
pid = spawn_link(fn -> sse_run(req, self(), ref) end)
%{ref: ref, pid: pid, buf: ""}
end,
fn state ->
receive do
{ref, :data, data} when ref == state.ref ->
{events, buf} = parse_sse(state.buf <> data)
{events, %{state | buf: buf}}
{ref, :done} when ref == state.ref ->
{:halt, state}
{ref, :error, error} when ref == state.ref ->
{[{:error, error}], state}
end
end,
fn state ->
case Process.alive?(state.pid) do
true -> Process.exit(state.pid, :normal)
false -> :ok
end
end
)
end
defp sse_run(req, pid, ref) do
case Req.request(req,
into: fn {:data, data}, acc ->
send(pid, {ref, :data, data})
{:cont, acc}
end
) do
{:ok, _} -> send(pid, {ref, :done})
{:error, error} -> send(pid, {ref, :error, error})
end
end
defp parse_sse(data) do
parts = String.split(data, "\n\n")
count = length(parts)
case count do
0 -> {[], ""}
1 -> {[], data}
_ ->
events = parts |> Enum.drop(-1) |> Enum.flat_map(&decode_sse/1)
{events, List.last(parts)}
end
end
defp decode_sse(chunk) do
data =
chunk
|> String.split("\n")
|> Enum.filter(&String.starts_with?(&1, "data:"))
|> Enum.map(&String.replace_prefix(&1, "data:", ""))
|> Enum.join("\n")
|> String.trim()
case data do
"" -> []
_ ->
case Jason.decode(data) do
{:ok, val} -> [val]
{:error, _} -> [data]
end
end
end
end