Packages

A gleam dev proxy to enable live reload and smoother DX

Current section

Files

Jump to
olive src olive config.gleam
Raw

src/olive/config.gleam

import filepath
import gleam/dict
import gleam/erlang/atom.{type Atom}
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import olive/cli
import olive/logging
import simplifile
import tom
pub type Directory {
SourceDirectory(path: String)
PrivDirectory(path: String)
}
/// Holds all the important bits to run olive
pub type Config {
Config(
logger: logging.Logger,
/// The main port needs to match the one listen by the main server
main_port: Int,
/// The proxy port is used by olive to create the web proxy server
proxy_port: Int,
/// The bind address needs to match the one used by the main server
/// and is used by the proxy to replicate the same behaviour
bind: String,
/// The root dir, where gleam.toml lives
root: String,
/// The name of the project (field "name" in gleam.toml)
name: String,
/// The list of dirs olive needs to watch to trigger live reload
dirs: List(Directory),
/// The debounce in ms to use for file watchers
debounce_in_ms: Int,
)
}
@external(erlang, "olive_ffi", "get_cwd")
fn get_cwd() -> Result(String, Atom)
pub fn read_config(
logger: logging.Logger,
options: cli.CliOptions,
) -> Result(Config, String) {
logging.notice(
logger,
"Reading gleam.toml to retrieve project's name and directories to watch",
)
use root <- result.try(get_gleam_toml_path())
use gleam_toml <- result.try(read_gleam_toml(root))
use name <- result.try(read_project_name(gleam_toml))
use deps <- result.try(read_project_dependencies(gleam_toml, root))
logging.debug(
logger,
"Olive will watch for changes in those folders:\n" <> print_deps(deps),
)
Ok(Config(
logger:,
root:,
name:,
dirs: deps,
bind: options.bind,
main_port: options.main_port,
proxy_port: options.proxy_port,
debounce_in_ms: options.debounce_in_ms,
))
}
type TomlFile =
dict.Dict(String, tom.Toml)
fn read_project_name(toml: TomlFile) {
tom.get_string(toml, ["name"]) |> result.map_error(tom_field_error_to_string)
}
fn read_project_dependencies(toml: TomlFile, root: String) {
// We want path dependencies only.
// Path dependencies such as `common = { path = "../common" }` will be
// watched at "../common/src" as well as the main src folder.
let source_directory = check_source_directory(root)
let priv_directory = check_priv_directory(root)
tom.get_table(toml, ["dependencies"])
|> result.map_error(tom_field_error_to_string)
|> result.map(fn(deps) {
deps
|> dict.fold(
from: [source_directory, priv_directory],
with: fn(acc, _key, value) {
case value {
tom.InlineTable(table) -> {
case dict.get(table, "path") {
Error(_) -> acc
Ok(tom.String(path)) -> {
let full_path = filepath.join(root, path)
let source_dir = check_source_directory(full_path)
let priv_dir = check_priv_directory(full_path)
[source_dir, priv_dir, ..acc]
}
Ok(_) -> acc
}
}
_ -> acc
}
},
)
|> option.values
})
}
fn check_source_directory(root: String) {
check_directory(filepath.join(root, "src"), SourceDirectory)
}
fn check_priv_directory(root: String) {
check_directory(filepath.join(root, "priv"), PrivDirectory)
}
fn check_directory(path: String, ctor: fn(String) -> Directory) {
case filepath.expand(path) {
Ok(full_path) -> {
case simplifile.is_directory(full_path) {
Ok(True) -> option.Some(ctor(full_path))
Ok(False) | Error(_) -> option.None
}
}
Error(Nil) -> option.None
}
}
fn get_gleam_toml_path() -> Result(String, String) {
get_cwd()
|> result.map_error(cwd_error_to_string)
|> result.then(get_root)
}
fn get_root(path: String) -> Result(String, String) {
case simplifile.is_file(filepath.join(path, "gleam.toml")) {
Ok(True) -> Ok(path)
Ok(False) | Error(_) -> {
case filepath.expand(filepath.join(path, "..")) {
Ok(path) -> get_root(path)
Error(_) -> Error("Could not locate root dir where gleam.toml is")
}
}
}
}
fn read_gleam_toml(path: String) -> Result(TomlFile, String) {
filepath.join(path, "gleam.toml")
|> simplifile.read
|> result.map_error(simplifile_error_to_string)
|> result.try(fn(content) {
tom.parse(content)
|> result.map_error(tom_error_to_string)
})
}
fn print_deps(deps: List(Directory)) {
string.join(
list.map(deps, fn(dir) {
case dir {
PrivDirectory(path) -> "Any changes in: " <> path
SourceDirectory(path) -> "Code changes in: " <> path
}
}),
"\n",
)
}
// ERROR MAPPERS -------------------------------------------------
fn cwd_error_to_string(posix_error: Atom) -> String {
"Could not get current working dir, error is: " <> atom.to_string(posix_error)
}
fn simplifile_error_to_string(err: simplifile.FileError) -> String {
"Could not read gleam.toml: " <> simplifile.describe_error(err)
}
fn tom_error_to_string(parse_error: tom.ParseError) -> String {
case parse_error {
tom.KeyAlreadyInUse(keys) ->
"Could not parse gleam.toml: Key already in use: "
<> string.join(keys, ", ")
tom.Unexpected(char, _) ->
"Could not parse gleam.toml: Unexpected character: " <> char
}
}
fn tom_field_error_to_string(parse_error: tom.GetError) -> String {
case parse_error {
tom.NotFound(keys) ->
"Could not find " <> string.join(keys, ".") <> " in gleam.toml"
tom.WrongType(keys, expected, got) ->
"Could not read "
<> string.join(keys, ".")
<> ". Expected "
<> expected
<> " but got "
<> got
}
}