Current section

Files

Jump to
gleamrpc_http_server src gleamrpc http server.gleam
Raw

src/gleamrpc/http/server.gleam

import convert
import convert/http/query
import convert/json as cjson
import gleam/bytes_tree
import gleam/dynamic
import gleam/http
import gleam/http/request
import gleam/http/response
import gleam/json
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import gleamrpc
import mist
/// Custom errors generated by this server
pub type HttpServerError {
MethodUnsupported(method: http.Method)
NoNameError
WrongPrefix(path: String)
}
/// Return a GleamRPC server implementation that uses HTTP requests.
/// To be used with `gleamrpc.with_server`
pub fn http_server() -> gleamrpc.ProcedureServer(
request.Request(mist.Connection),
response.Response(mist.ResponseData),
HttpServerError,
) {
gleamrpc.ProcedureServer(
get_identity:,
get_params:,
recover_error:,
encode_result:,
)
}
fn get_identity(
in_request: request.Request(_),
) -> Result(
gleamrpc.ProcedureIdentity,
gleamrpc.GleamRPCServerError(HttpServerError),
) {
use type_ <- result.try(extract_procedure_type(in_request))
use #(name, router) <- result.try(extract_procedure_name_and_router(
in_request,
))
Ok(gleamrpc.ProcedureIdentity(name, router, type_))
}
fn extract_procedure_type(
in_request: request.Request(_),
) -> Result(
gleamrpc.ProcedureType,
gleamrpc.GleamRPCServerError(HttpServerError),
) {
case in_request.method {
http.Get -> Ok(gleamrpc.Query)
http.Post -> Ok(gleamrpc.Mutation)
_ as method -> Error(gleamrpc.GetIdentityError(MethodUnsupported(method)))
}
}
fn extract_procedure_name_and_router(
in_request: request.Request(_),
) -> Result(
#(String, option.Option(gleamrpc.Router)),
gleamrpc.GleamRPCServerError(HttpServerError),
) {
case in_request |> request.path_segments {
["api", "gleamRPC", ..rest] -> build_name_and_router(rest, option.None)
_ -> Error(gleamrpc.GetIdentityError(WrongPrefix(in_request.path)))
}
}
fn build_name_and_router(
segments: List(String),
router: option.Option(gleamrpc.Router),
) -> Result(
#(String, option.Option(gleamrpc.Router)),
gleamrpc.GleamRPCServerError(HttpServerError),
) {
case segments {
[name] -> Ok(#(name, router))
[name, ..rest] -> {
build_name_and_router(rest, option.Some(gleamrpc.Router(name, router)))
}
[] -> Error(gleamrpc.GetIdentityError(NoNameError))
}
}
fn get_params(
request: request.Request(mist.Connection),
) -> fn(gleamrpc.ProcedureType, convert.GlitrType) ->
Result(convert.GlitrValue, gleamrpc.GleamRPCServerError(HttpServerError)) {
fn(procedure_type: gleamrpc.ProcedureType, params_type: convert.GlitrType) {
case procedure_type {
gleamrpc.Mutation -> get_params_mutation(request, params_type)
gleamrpc.Query -> get_params_query(request, params_type)
}
}
}
fn get_params_query(
request: request.Request(mist.Connection),
params_type: convert.GlitrType,
) -> Result(convert.GlitrValue, gleamrpc.GleamRPCServerError(HttpServerError)) {
request.get_query(request)
|> result.replace_error(
gleamrpc.GetParamsError([dynamic.DecodeError("A valid query", "", [])]),
)
|> result.then(fn(query_value) {
query_value
|> query.decode_value(params_type)
|> result.map_error(gleamrpc.GetParamsError)
})
}
fn get_params_mutation(
request: request.Request(mist.Connection),
params_type: convert.GlitrType,
) -> Result(convert.GlitrValue, gleamrpc.GleamRPCServerError(HttpServerError)) {
request
|> mist.read_body(65_000)
|> result.map_error(mist_read_error_to_get_params_error)
|> result.then(decode_bit_array_request(_, params_type))
}
fn mist_read_error_to_get_params_error(
err: mist.ReadError,
) -> gleamrpc.GleamRPCServerError(_) {
case err {
mist.ExcessBody ->
gleamrpc.GetParamsError([
dynamic.DecodeError("Body size <= 65000", "", []),
])
mist.MalformedBody ->
gleamrpc.GetParamsError([
dynamic.DecodeError("A valid body", "Malformed body", []),
])
}
}
fn decode_bit_array_request(
request: request.Request(BitArray),
params_type: convert.GlitrType,
) -> Result(convert.GlitrValue, gleamrpc.GleamRPCServerError(HttpServerError)) {
request.body
|> json.decode_bits(cjson.decode_value(params_type))
|> result.map_error(fn(err) {
case err {
json.UnableToDecode(decode_err) ->
decode_err
|> list.map(fn(err) {
dynamic.DecodeError(err.expected, err.found, err.path)
})
|> gleamrpc.GetParamsError
json.UnexpectedByte(byte) ->
gleamrpc.GetParamsError([
dynamic.DecodeError("A valid JSON", "Unexpected byte : " <> byte, []),
])
json.UnexpectedEndOfInput ->
gleamrpc.GetParamsError([
dynamic.DecodeError("A valid JSON", "Unexpected end of input", []),
])
json.UnexpectedFormat(decode_err) -> gleamrpc.GetParamsError(decode_err)
json.UnexpectedSequence(seq) ->
gleamrpc.GetParamsError([
dynamic.DecodeError(
"A valid JSON",
"Unexpected sequence : " <> seq,
[],
),
])
}
})
}
fn recover_error(
error: gleamrpc.GleamRPCServerError(HttpServerError),
) -> response.Response(mist.ResponseData) {
case error {
gleamrpc.GetIdentityError(err) -> recover_http_server_error(err)
gleamrpc.GetParamsError(errors) -> recover_decode_errors(errors)
gleamrpc.ProcedureExecError(err) -> response_from_string(500, err.message)
gleamrpc.WrongProcedure -> response_from_string(404, "Procedure not found")
}
}
fn recover_http_server_error(
error: HttpServerError,
) -> response.Response(mist.ResponseData) {
case error {
MethodUnsupported(method) ->
response_from_string(
405,
method |> http.method_to_string <> " not allowed",
)
NoNameError -> response_from_string(400, "Couldn't extract procedure name")
WrongPrefix(path) -> response_from_string(400, "Wrong prefix: " <> path)
}
}
fn recover_decode_errors(
errors: List(dynamic.DecodeError),
) -> response.Response(mist.ResponseData) {
errors
|> list.map(fn(error) {
"Expected : "
<> error.expected
<> ", got : "
<> error.found
<> " at "
<> string.join(error.path, ".")
})
|> string.join("\n")
|> response_from_string(400, _)
}
fn response_from_string(
response_code: Int,
body: String,
) -> response.Response(mist.ResponseData) {
response.Response(
response_code,
[],
mist.Bytes(body |> bytes_tree.from_string),
)
}
fn encode_result(
result: convert.GlitrValue,
) -> response.Response(mist.ResponseData) {
let body =
cjson.encode_value(result)
|> json.to_string
|> bytes_tree.from_string
|> mist.Bytes
response.new(200)
|> response.set_body(body)
|> response.set_header("Content-Type", "application/json")
}
/// Using a ProcedureServerInstance, serve GleamRPC requests over HTTP with Mist
/// Example:
///
/// ```gleam
/// gleamrpc.with_server(http_server())
/// |> gleamrpc.with_context(create_context)
/// |> gleamrpc.with_implementation(create_user_procedure, create_user)
/// |> gleamrpc.with_implementation(get_user_procedure, get_user)
/// |> init_mist(8080)
/// |> mist.start_http()
/// ```
pub fn init_mist(
server: gleamrpc.ProcedureServerInstance(
request.Request(mist.Connection),
response.Response(mist.ResponseData),
_,
HttpServerError,
),
port: Int,
) -> mist.Builder(mist.Connection, mist.ResponseData) {
server
|> gleamrpc.serve()
|> mist.new()
|> mist.port(port)
}
pub type CorsOrigins {
AllOrigins
CorsOrigins(origins: List(String))
}
/// A middleware that can be registered on a GleamRPC HTTP server to allow cross origin requests.
/// You can allow requests from all origins using the `AllOrigins` option.
/// Otherwise, use `CorsOrigins([...])` to allow a list of allowed origins.
pub fn cors_middleware(
origins: CorsOrigins,
) -> gleamrpc.ProcedureServerMiddleware(
request.Request(mist.Connection),
response.Response(mist.ResponseData),
) {
fn(
in: request.Request(mist.Connection),
next: fn(request.Request(mist.Connection)) ->
response.Response(mist.ResponseData),
) -> response.Response(mist.ResponseData) {
let response = next(in)
case origins {
AllOrigins ->
response |> response.set_header("Access-Control-Allow-Origin", "*")
CorsOrigins(origins) -> {
case
list.key_find(in.headers, "origin"),
list.key_find(in.headers, "Origin")
{
Error(_), Error(_) -> response
Ok(origin), _ | _, Ok(origin) ->
case list.contains(origins, origin) {
False -> response
True ->
response
|> response.set_header("Access-Control-Allow-Origin", origin)
}
}
}
}
}
}