Initial commit

This commit is contained in:
Mikko Ahlroth 2023-01-28 16:55:39 +02:00
commit fa03659c08
11 changed files with 491 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.beam
*.ez
build
erl_crash.dump

3
.tool-versions Normal file
View file

@ -0,0 +1,3 @@
gleam 0.26.1
erlang 25.1.2
elixir 1.14.3-otp-25

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# finch_gleam
[![Package Version](https://img.shields.io/hexpm/v/finch_gleam)](https://hex.pm/packages/finch_gleam)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/finch_gleam/)
A Gleam project
## Quick start
```sh
gleam run # Run the project
gleam test # Run the tests
gleam shell # Run an Erlang shell
```
## Installation
If available on Hex this package can be added to your Gleam project:
```sh
gleam add finch_gleam
```
and its documentation can be found at <https://hexdocs.pm/finch_gleam>.

20
gleam.toml Normal file
View file

@ -0,0 +1,20 @@
name = "finch_gleam"
version = "1.0.0"
# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
licences = ["MIT"]
description = "Gleam wrapper for the Finch HTTP client"
repository = { type = "gitlab", user = "Nicd", repo = "gleam_finch" }
links = []
[dependencies]
gleam_stdlib = "~> 0.25"
gleam_erlang = "~> 0.17"
gleam_http = "~> 3.1"
finch = "~> 0.14"
[dev-dependencies]
gleeunit = "~> 0.7"
mist = "~> 0.9"

28
manifest.toml Normal file
View file

@ -0,0 +1,28 @@
# This file was generated by Gleam
# You typically do not need to edit this file
packages = [
{ name = "castore", version = "0.1.22", build_tools = ["mix"], requirements = [], otp_app = "castore", source = "hex", outer_checksum = "C17576DF47EB5AA1EE40CC4134316A99F5CAD3E215D5C77B8DD3CFEF12A22CAC" },
{ name = "finch", version = "0.14.0", build_tools = ["mix"], requirements = ["telemetry", "mime", "castore", "mint", "nimble_pool", "nimble_options"], otp_app = "finch", source = "hex", outer_checksum = "5459ACAF18C4FDB47A8C22FB3BAFF5D8173106217C8E56C5BA0B93E66501A8DD" },
{ name = "gleam_erlang", version = "0.17.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "BAAA84F5BCC4477E809BA3E03BB3009A3894A6544C1511626C44408E39DB2AE6" },
{ name = "gleam_http", version = "3.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B66B7A1539CCB577119E4DC80DD3484C1A652CB032967954498EEDBAE3355763" },
{ name = "gleam_otp", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "24B88BF1D5B8DEC2525C00ECB65B96D2FD4DC66D8B2BB4D7AD4D12B2CE2A9988" },
{ name = "gleam_stdlib", version = "0.26.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6221F9D7A08B6D6DBCDD567B2BB7C4B2A7BBF4C04C6110757BE04635143BDEC8" },
{ name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" },
{ name = "glisten", version = "0.6.9", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "83CDA9C9FCD316ECF65DBEFAF5690999DDC0FE8CB31696711E7D60FD995E03B7" },
{ name = "hpax", version = "0.1.2", build_tools = ["mix"], requirements = [], otp_app = "hpax", source = "hex", outer_checksum = "2C87843D5A23F5F16748EBE77969880E29809580EFDACCD615CD3BED628A8C13" },
{ name = "mime", version = "2.0.3", build_tools = ["mix"], requirements = [], otp_app = "mime", source = "hex", outer_checksum = "27A30BF0DB44D25EECBA73755ACF4068CBFE26A4372F9EB3E4EA3A45956BFF6B" },
{ name = "mint", version = "1.4.2", build_tools = ["mix"], requirements = ["castore", "hpax"], otp_app = "mint", source = "hex", outer_checksum = "CE75A5BBCC59B4D7D8D70F8B2FC284B1751FFB35C7B6A6302B5192F8AB4DDD80" },
{ name = "mist", version = "0.9.4", build_tools = ["gleam"], requirements = ["glisten", "gleam_erlang", "gleam_otp", "gleam_http", "gleam_stdlib"], otp_app = "mist", source = "hex", outer_checksum = "D327F690A717F588281E636D99E014A90D8EBC2F2B479AA799A1D6AA3899A1B2" },
{ name = "nimble_options", version = "0.5.2", build_tools = ["mix"], requirements = [], otp_app = "nimble_options", source = "hex", outer_checksum = "4DA7F904B915FD71DB549BCDC25F8D56F378EF7AE07DC1D372CBE72BA950DCE0" },
{ name = "nimble_pool", version = "0.2.6", build_tools = ["mix"], requirements = [], otp_app = "nimble_pool", source = "hex", outer_checksum = "1C715055095D3F2705C4E236C18B618420A35490DA94149FF8B580A2144F653F" },
{ name = "telemetry", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "DAD9CE9D8EFFC621708F99EAC538EF1CBE05D6A874DD741DE2E689C47FEAFED5" },
]
[requirements]
finch = "~> 0.14"
gleam_erlang = "~> 0.17"
gleam_http = "~> 3.1"
gleam_stdlib = "~> 0.25"
gleeunit = "~> 0.7"
mist = "~> 0.9"

150
src/finch.gleam Normal file
View file

@ -0,0 +1,150 @@
//// A wrapper module for the Finch HTTP client
////
//// https://hexdocs.pm/finch
import gleam
import gleam/uri
import gleam/erlang/atom.{Atom}
import gleam/http
import gleam/http/request as gleam_request
import gleam/http/response as gleam_response
import finch/headers.{Headers}
import finch/stream.{StreamFun}
import finch/otp.{InstanceOptions, OnStart}
pub type Method {
Head
Get
Put
Post
Patch
Delete
Options
}
pub type RequestOption {
PoolTimeout(ms: Int)
RequestTimeout(ms: Int)
}
pub type RequestOptions =
List(RequestOption)
/// Finch request, use `build/2` to create one from a Gleam request
pub external type Request
external type Response
/// Exception returned by Finch in case of request errors
pub external type Exception
/// Build a Finch request that can be run with `request/2` or `stream/5`
///
/// Note, the HTTP methods CONNECT, TRACE, and `Other(String)` are not
/// supported, and will be converted to GET.
pub fn build(
req: gleam_request.Request(String),
opts: RequestOptions,
) -> Request {
let url_str =
req
|> gleam_request.to_uri()
|> uri.to_string()
build_ext(
gleam_method_to_finch_method(req.method),
url_str,
req.headers,
req.body,
opts,
)
}
external fn build_ext(
method: Method,
url: String,
headers: Headers,
body: String,
opts: RequestOptions,
) -> Request =
"Elixir.Finch" "build"
/// Send a request using the Finch server with the given name
pub fn request(
req: Request,
name: Atom,
) -> Result(gleam_response.Response(String), Exception) {
try resp = do_request(req, name)
gleam.Ok(gleam_response.Response(
body: response_body(resp),
headers: response_headers(resp),
status: response_status(resp),
))
}
external fn do_request(req: Request, name: Atom) -> Result(Response, Exception) =
"Elixir.Finch" "request"
/// Stream a request using the Finch server with the given name
pub external fn stream(
req: Request,
name: Atom,
acc: a,
fun: StreamFun(a),
opts: RequestOptions,
) -> Result(a, Exception) =
"Elixir.Finch" "stream"
/// Start a new Finch server with the given options
pub external fn start_link(opts: InstanceOptions) -> OnStart =
"Elixir.Finch" "start_link"
fn response_body(resp: Response) -> String {
get_response_body(resp, Body)
}
fn response_headers(resp: Response) -> Headers {
get_response_headers(resp, Headers)
}
fn response_status(resp: Response) -> Int {
get_response_status(resp, Status)
}
fn gleam_method_to_finch_method(method: http.Method) -> Method {
case method {
http.Get -> Get
http.Head -> Head
http.Post -> Post
http.Put -> Put
http.Patch -> Patch
http.Delete -> Delete
http.Options -> Options
_other -> Get
}
}
type ResponseBodyKey {
Body
}
type ResponseHeadersKey {
Headers
}
type ResponseStatusKey {
Status
}
external fn get_response_body(resp: Response, key: ResponseBodyKey) -> String =
"Elixir.Map" "fetch!"
external fn get_response_status(resp: Response, key: ResponseStatusKey) -> Int =
"Elixir.Map" "fetch!"
external fn get_response_headers(
resp: Response,
key: ResponseHeadersKey,
) -> Headers =
"Elixir.Map" "fetch!"

5
src/finch/headers.gleam Normal file
View file

@ -0,0 +1,5 @@
pub type Header =
#(String, String)
pub type Headers =
List(Header)

55
src/finch/otp.gleam Normal file
View file

@ -0,0 +1,55 @@
//// Types related to OTP usage of Finch
import gleam/uri
import gleam/dynamic.{Dynamic}
import gleam/map.{Map}
import gleam/erlang/atom.{Atom}
import gleam/erlang/process.{Pid}
import finch/timeout.{Timeout}
pub type UnixSocketPath {
Local(path: String)
}
pub type UnixSocketSpecifier {
Http(path: UnixSocketPath)
Https(path: UnixSocketPath)
}
pub type PoolSpecifier {
Default
URL(url: uri.Uri)
UnixSocket(specifier: UnixSocketSpecifier)
}
pub type PoolProtocol {
Http1
Http2
}
pub type PoolOption {
Protocol(PoolProtocol)
Size(Int)
Count(Int)
MaxIdleTime(Timeout)
ConnOpts(List(#(Atom, Dynamic)))
PoolMaxIdleTime(Timeout)
ConnMaxIdleTime(Timeout)
}
pub type PoolOptions =
List(PoolOption)
pub type InstanceOption {
Name(Atom)
Pools(Map(PoolSpecifier, PoolOptions))
}
pub type InstanceOptions =
List(InstanceOption)
pub type OnStart {
Ok(child: Pid)
Ignore
Error(Dynamic)
}

10
src/finch/stream.gleam Normal file
View file

@ -0,0 +1,10 @@
import finch/headers.{Headers as ReqHeaders}
pub type StreamBlock {
Status(status: Int)
Headers(headers: ReqHeaders)
Data(data: BitString)
}
pub type StreamFun(a) =
fn(StreamBlock, a) -> a

25
src/finch/timeout.gleam Normal file
View file

@ -0,0 +1,25 @@
//// Wrapper of Elixir's `timeout/0` type
////
//// See https://hexdocs.pm/elixir/typespecs.html#built-in-types
import gleam/dynamic
/// A timeout value, either infinite or a specified amount of milliseconds
pub external type Timeout
/// Create an infinite timeout
pub fn infinity() -> Timeout {
dynamic.from(Infinity)
|> dynamic.unsafe_coerce()
}
/// Create a timeout with the given milliseconds
pub fn milliseconds(ms: Int) -> Timeout {
dynamic.from(ms)
|> dynamic.unsafe_coerce()
}
// Janky way to create the atom "infinity"
type InfinityAtom {
Infinity
}

167
test/finch_gleam_test.gleam Normal file
View file

@ -0,0 +1,167 @@
import gleeunit
import gleeunit/should
import mist
import gleam/bit_builder
import gleam/http
import gleam/http/request
import gleam/http/response
import gleam/erlang/atom.{Atom}
import gleam/erlang/process
import gleam/uri
import gleam/string
import gleam/list
import finch
import finch/otp
const base_url = "http://localhost:33100"
pub fn main() {
assert Ok(_) =
mist.run_service(
33_100,
fn(req) {
case req.method, request.path_segments(req) {
http.Get, ["ok"] ->
response.new(200)
|> response.set_body(bit_builder.from_string("OK"))
http.Post, ["post"] ->
response.new(500)
|> response.set_body(bit_builder.from_bit_string(req.body))
http.Delete, ["headers"] ->
response.new(200)
|> response.set_body(bit_builder.from_string(string.inspect(
req.headers,
)))
|> response.set_header("x-token", "token-x")
}
},
max_body_limit: 4_000_000,
)
configure_logger([Level(None)])
gleeunit.main()
}
pub fn ok_req_test() {
use <- with_finch()
assert Ok(url) = uri.parse(ok_url())
assert Ok(req) = request.from_uri(url)
let finch_req = finch.build(req, [])
assert Ok(resp) = finch.request(finch_req, server_name())
should.equal(resp.status, 200)
should.equal(resp.body, "OK")
}
pub fn post_data_test() {
let body = "FOOBAR"
use <- with_finch()
assert Ok(url) = uri.parse(post_url())
assert Ok(req) = request.from_uri(url)
let req = request.Request(..req, method: http.Post, body: body)
let finch_req = finch.build(req, [])
assert Ok(resp) = finch.request(finch_req, server_name())
should.equal(resp.status, 500)
should.equal(resp.body, body)
}
pub fn headers_test() {
use <- with_finch()
assert Ok(url) = uri.parse(headers_url())
assert Ok(req) = request.from_uri(url)
let req =
request.Request(
..req,
method: http.Delete,
headers: [
#("accept", "multipart/form-data"),
#("content-type", "formipart/mult-data"),
],
)
let finch_req = finch.build(req, [])
assert Ok(resp) = finch.request(finch_req, server_name())
should.equal(resp.status, 200)
should.be_true(string.contains(
resp.body,
"#(\"accept\", \"multipart/form-data\")",
))
should.be_true(string.contains(
resp.body,
"#(\"content-type\", \"formipart/mult-data\")",
))
should.be_true(list.contains(resp.headers, #("x-token", "token-x")))
}
fn ok_url() {
base_url <> "/ok"
}
fn post_url() {
base_url <> "/post"
}
fn headers_url() {
base_url <> "/headers"
}
fn with_finch(test: fn() -> a) {
let subject: process.Subject(process.Pid) = process.new_subject()
process.start(
fn() {
assert otp.Ok(child) = start_finch()
process.send(subject, child)
process.sleep_forever()
},
False,
)
assert Ok(finch_pid) = process.receive(subject, 1000)
let monitor = process.monitor_process(finch_pid)
let selector =
process.new_selector()
|> process.selecting_process_down(monitor, fn(down) { down })
test()
process.kill(finch_pid)
assert Ok(_) = process.select(selector, 1000)
// We need to wait for all the Finch processes to close, sadly didn't find a
// better way to do this :/
process.sleep(200)
}
fn start_finch() -> otp.OnStart {
let name = server_name()
finch.start_link([otp.Name(name)])
}
fn server_name() -> Atom {
atom.create_from_string("finch_test_server")
}
type LogLevel {
None
}
type LoggerOption {
Level(LogLevel)
}
type LoggerOptions =
List(LoggerOption)
external fn configure_logger(LoggerOptions) -> Nil =
"Elixir.Logger" "configure"