Skip to content

Commit

Permalink
Hello, Joe!
Browse files Browse the repository at this point in the history
  • Loading branch information
lpil committed Dec 11, 2021
0 parents commit 027961c
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
build
erl_crash.dump
45 changes: 45 additions & 0 deletions README.md
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
```
15 changes: 15 additions & 0 deletions gleam.toml
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"
18 changes: 18 additions & 0 deletions manifest.toml
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"
5 changes: 5 additions & 0 deletions src/nerf.gleam
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!")
}
43 changes: 43 additions & 0 deletions src/nerf/gun.gleam
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
}
63 changes: 63 additions & 0 deletions src/nerf/websocket.gleam
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)
}
35 changes: 35 additions & 0 deletions src/nerf_ffi.erl
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.
29 changes: 29 additions & 0 deletions test/nerf_test.gleam
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)
}

0 comments on commit 027961c

Please sign in to comment.