-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 027961c
Showing
10 changed files
with
280 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: test | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
- main | ||
pull_request: | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/[email protected] | ||
- uses: gleam-lang/[email protected] | ||
with: | ||
otp-version: "23.2" | ||
- uses: gleam-lang/[email protected] | ||
with: | ||
gleam-version: "0.18.0" | ||
- run: gleam format --check src test | ||
- run: gleam deps download | ||
- run: gleam test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
*.beam | ||
*.ez | ||
build | ||
erl_crash.dump |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# nerf | ||
|
||
Gleam bindings to the Erlang [gun][gun] HTTP/1.1, HTTP/2 and Websocket client. | ||
|
||
[gun]: https://hex.pm/packages/gun | ||
|
||
Currently this library is rather basic and only supports a portion of the | ||
websocket API. Let us know if you need more. | ||
|
||
## Usage | ||
|
||
This package can be added to your Gleam project like so. | ||
|
||
```sh | ||
gleam add nerf | ||
``` | ||
|
||
Then use it in your Gleam application. | ||
|
||
```rust | ||
import nerf/websocket | ||
|
||
pub fn main() { | ||
// Connect | ||
assert Ok(conn) = websocket.connect("example.com", "/ws", 8080, []) | ||
|
||
// Send some messages | ||
websocket.send(conn, "Hello") | ||
websocket.send(conn, "World") | ||
|
||
// Receive some messages | ||
assert Ok(Text("Hello")) = websocket.receive(conn, 500) | ||
assert Ok(Text("World")) = websocket.receive(conn, 500) | ||
|
||
// Close the connection | ||
websocket.close(conn) | ||
} | ||
``` | ||
|
||
## Testing this library | ||
|
||
```sh | ||
podman run --rm --detach -p 8080:8080 --name echo jmalloc/echo-server | ||
gleam test | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
name = "nerf" | ||
version = "0.1.0" | ||
licences = ["Apache-2.0"] | ||
description = "Gleam bindings to the gun HTTP/1.1, HTTP/2 and Websocket client" | ||
repository = { type = "github", user = "lpil", repo = "nerf" } | ||
links = [{ title = "Website", href = "https://gleam.run" }] | ||
|
||
[dependencies] | ||
gleam_stdlib = "~> 0.18" | ||
gun = "> 1.3.0 and < 3.0.0" | ||
gleam_http = "~> 2.1" | ||
gleam_erlang = "~> 0.5" | ||
|
||
[dev-dependencies] | ||
gleeunit = "~> 0.5" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# This file was generated by Gleam | ||
# You typically do not need to edit this file | ||
|
||
packages = [ | ||
{ name = "cowlib", version = "2.7.3", build_tools = ["rebar3"], requirements = [], otp_app = "cowlib", source = "hex", outer_checksum = "1E1A3D176D52DAEBBECBBCDFD27C27726076567905C2A9D7398C54DA9D225761" }, | ||
{ name = "gleam_erlang", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "6DC8D506127BF332A48DBE1A286ACAAA0F2C7BF84B33BD9E6F3A8002EC9F649C" }, | ||
{ name = "gleam_http", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "C2DF02A2BD551B590D92ADA90A67CDEE60EB4BAD48B5EE10A9AB4CE180CBCE36" }, | ||
{ name = "gleam_stdlib", version = "0.18.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2938F996BBB25D75E973226846CDDFA33AB5590AFC8A9D043A356EA85272510D" }, | ||
{ name = "gleeunit", version = "0.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7FA7477D930178C1E59519DBDB5E086BE3A6B65F015B67DA94D30A323062154" }, | ||
{ name = "gun", version = "1.3.3", build_tools = ["rebar3"], requirements = ["cowlib"], otp_app = "gun", source = "hex", outer_checksum = "3106CE167F9C9723F849E4FB54EA4A4D814E3996AE243A1C828B256E749041E0" }, | ||
] | ||
|
||
[requirements] | ||
gleam_erlang = "~> 0.5" | ||
gleam_http = "~> 2.1" | ||
gleam_stdlib = "~> 0.18" | ||
gleeunit = "~> 0.5" | ||
gun = "> 1.3.0 and < 3.0.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import gleam/io | ||
|
||
pub fn main() { | ||
io.println("Hello from nerf!") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
//// Low level bindings to the gun API. You typically do not need to use this. | ||
//// Prefer the other modules in this library. | ||
|
||
import gleam/http.{Header} | ||
import gleam/erlang/charlist.{Charlist} | ||
import gleam/dynamic.{Dynamic} | ||
|
||
pub external type StreamReference | ||
|
||
pub external type ConnectionPid | ||
|
||
pub fn open(host: String, port: Int) -> Result(ConnectionPid, Dynamic) { | ||
open_erl(charlist.from_string(host), port) | ||
} | ||
|
||
pub external fn open_erl(Charlist, Int) -> Result(ConnectionPid, Dynamic) = | ||
"gun" "open" | ||
|
||
pub external fn await_up(ConnectionPid) -> Result(Dynamic, Dynamic) = | ||
"gun" "await_up" | ||
|
||
pub external fn ws_upgrade( | ||
ConnectionPid, | ||
String, | ||
List(Header), | ||
) -> StreamReference = | ||
"gun" "ws_upgrade" | ||
|
||
pub type Frame { | ||
Close | ||
Text(String) | ||
Binary(BitString) | ||
} | ||
|
||
external type OkAtom | ||
|
||
external fn ws_send_erl(ConnectionPid, Frame) -> OkAtom = | ||
"gun" "ws_send" | ||
|
||
pub fn ws_send(pid: ConnectionPid, frame: Frame) -> Nil { | ||
ws_send_erl(pid, frame) | ||
Nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import gleam/uri.{Uri} | ||
import gleam/http.{Header} | ||
import gleam/dynamic.{Dynamic} | ||
import gleam/result | ||
import nerf/gun.{ConnectionPid, StreamReference} | ||
|
||
pub opaque type Connection { | ||
Connection(ref: StreamReference, pid: ConnectionPid) | ||
} | ||
|
||
pub type Frame { | ||
Close | ||
Text(String) | ||
Binary(BitString) | ||
} | ||
|
||
pub fn connect( | ||
hostname: String, | ||
path: String, | ||
on port: Int, | ||
with headers: List(Header), | ||
) -> Result(Connection, ConnectError) { | ||
try pid = | ||
gun.open(hostname, port) | ||
|> result.map_error(ConnectionFailed) | ||
try _ = | ||
gun.await_up(pid) | ||
|> result.map_error(ConnectionFailed) | ||
|
||
// Upgrade to websockets | ||
let ref = gun.ws_upgrade(pid, path, headers) | ||
let conn = Connection(pid: pid, ref: ref) | ||
try _ = | ||
await_upgrade(conn, 1000) | ||
|> result.map_error(ConnectionFailed) | ||
|
||
// TODO: handle upgrade failure | ||
// https://ninenines.eu/docs/en/gun/2.0/guide/websocket/ | ||
// https://ninenines.eu/docs/en/gun/1.2/manual/gun_error/ | ||
// https://ninenines.eu/docs/en/gun/1.2/manual/gun_response/ | ||
Ok(conn) | ||
} | ||
|
||
pub fn send(to conn: Connection, this message: String) -> Nil { | ||
gun.ws_send(conn.pid, gun.Text(message)) | ||
} | ||
|
||
pub external fn receive(from: Connection, within: Int) -> Result(Frame, Nil) = | ||
"nerf_ffi" "ws_receive" | ||
|
||
external fn await_upgrade(from: Connection, within: Int) -> Result(Nil, Dynamic) = | ||
"nerf_ffi" "ws_await_upgrade" | ||
|
||
// TODO: listen for close events | ||
pub fn close(conn: Connection) -> Nil { | ||
gun.ws_send(conn.pid, gun.Close) | ||
} | ||
|
||
/// The URI of the websocket server to connect to | ||
pub type ConnectError { | ||
ConnectionRefused(status: Int, headers: List(Header)) | ||
ConnectionFailed(reason: Dynamic) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
-module(nerf_ffi). | ||
|
||
-export([ws_receive/2, ws_await_upgrade/2]). | ||
|
||
ws_receive({connection, Ref, Pid}, Timeout) | ||
when is_reference(Ref) andalso is_pid(Pid) -> | ||
receive | ||
{gun_ws, Pid, Ref, close} -> {ok, close}; | ||
{gun_ws, Pid, Ref, {close, _}} -> {ok, close}; | ||
{gun_ws, Pid, Ref, {close, _, _}} -> {ok, close}; | ||
{gun_ws, Pid, Ref, {text, _} = Frame} -> {ok, Frame}; | ||
{gun_ws, Pid, Ref, {binary, _} = Frame} -> {ok, Frame} | ||
after Timeout -> | ||
{error, nil} | ||
end. | ||
|
||
ws_await_upgrade({connection, Ref, Pid}, Timeout) | ||
when is_reference(Ref) andalso is_pid(Pid) -> | ||
receive | ||
{gun_upgrade, Pid, Ref, [<<"websocket">>], _} -> | ||
{ok, nil}; | ||
|
||
{gun_response, Pid, _, _, Status, Headers} -> | ||
% TODO: return an error | ||
exit({ws_upgrade_failed, Status, Headers}); | ||
|
||
{gun_error, Pid, Ref, Reason} -> | ||
% TODO: return an error | ||
exit({ws_upgrade_failed, Reason}) | ||
|
||
% TODO: Are other cases required? | ||
after Timeout -> | ||
% TODO: return an error | ||
exit(timeout) | ||
end. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import gleam/erlang | ||
import gleam/erlang/atom | ||
import gleam/string | ||
import gleeunit | ||
import gleeunit/should | ||
import nerf/websocket.{Text} | ||
|
||
pub fn main() { | ||
assert Ok(_) = erlang.ensure_all_started(atom.create_from_string("nerf")) | ||
gleeunit.main() | ||
} | ||
|
||
pub fn echo_test() { | ||
// Connect | ||
assert Ok(conn) = websocket.connect("localhost", "/ws", 8080, []) | ||
|
||
// The server we're using sends a little hello message | ||
assert Ok(Text(msg)) = websocket.receive(conn, 500) | ||
assert True = string.starts_with(msg, "Request served by ") | ||
|
||
// Send some messages, the test server echos them back | ||
websocket.send(conn, "Hello") | ||
websocket.send(conn, "World") | ||
assert Ok(Text("Hello")) = websocket.receive(conn, 500) | ||
assert Ok(Text("World")) = websocket.receive(conn, 500) | ||
|
||
// Close the connection | ||
websocket.close(conn) | ||
} |