import gleam/list
import gleam/result
import gleam/string
import shellout
import simplifile.{type FileError, File}
pub type ModuleChange {
Added(filename: String)
Updated(filename: String)
}
pub type ModuleError {
ModuleIsNotAFileError(String)
CannotReadModule(String, FileError)
CannotWriteModule(String, FileError)
CannotFormatError(String, #(Int, String))
}
const default_content: String = "// This file is auto-generated by iconify_lustre.
import gleam/list
import lustre/attribute.{type Attribute, attribute}
import lustre/element.{type Element}
import lustre/element/svg
fn add_defaults(attributes: List(Attribute(msg)),
defaults: List(Attribute(msg)),
) -> List(Attribute(msg)) {
list.fold(defaults, attributes, fn(accumulator, default) {
case list.any(accumulator, fn(item) { item.name == default.name }) {
False -> [default, ..accumulator]
True -> accumulator
}
})
}"
pub fn change_to_string(change: ModuleChange) {
case change {
Added(filename) -> "added to " <> filename
Updated(filename) -> "updated in " <> filename
}
}
pub fn error_to_string(error: ModuleError) {
case error {
ModuleIsNotAFileError(filename) ->
"Module '" <> filename <> "' exists but is not a file"
CannotReadModule(filename, error) ->
"Cannot read '" <> filename <> "', " <> simplifile.describe_error(error)
CannotWriteModule(filename, error) ->
"Cannot write '" <> filename <> "', " <> simplifile.describe_error(error)
CannotFormatError(filename, #(_, error)) ->
"Cannot format '" <> filename <> "', " <> error
}
}
// I'd love to use glance and glance_printer, but the latter isn't updated regularly,
// so I fall back to the original source: gleam and text processing (without explicit
// knowledge of the file-structure).
pub fn adjust(
filename: String,
fn_name: String,
fn_body: String,
) -> Result(ModuleChange, ModuleError) {
ensure_module(filename)
|> result.try(fn(_) { format_module(filename) })
|> result.try(fn(_) { update_module(fn_body, normalize(fn_name), filename) })
|> result.try(fn(result) { format_module(filename) |> result.replace(result) })
}
fn ensure_module(filename: String) -> Result(Nil, ModuleError) {
case simplifile.file_info(filename) {
Ok(info) ->
case simplifile.file_info_type(info) {
File -> Ok(Nil)
_ -> Error(ModuleIsNotAFileError(filename))
}
Error(_) ->
simplifile.write(filename, default_content)
|> result.map_error(CannotWriteModule(filename, _))
}
}
fn format_module(filename: String) -> Result(Nil, ModuleError) {
shellout.command("gleam", ["format", filename], in: ".", opt: [])
|> result.replace(Nil)
|> result.map_error(CannotFormatError(filename, _))
}
fn update_module(
fn_body: String,
fn_name: String,
filename: String,
) -> Result(ModuleChange, ModuleError) {
filename
|> read_lines()
|> result.map_error(CannotReadModule(filename, _))
|> result.map(split_module(fn_name, _))
|> result.try(fn(split) {
let #(before_function, function, after_function, epilogue) = split
[
before_function,
[
"pub fn "
<> fn_name
<> "(attributes: List(Attribute(msg))) -> Element(msg) {",
fn_body,
"}",
],
after_function,
epilogue,
]
|> list.flatten()
|> string.join("\n")
|> simplifile.write(filename, _)
|> result.map_error(CannotWriteModule(filename, _))
|> result.map(fn(_) {
case function {
[] -> Added(filename)
_ -> Updated(filename)
}
})
})
}
fn read_lines(filename: String) -> Result(List(String), FileError) {
filename
|> simplifile.read()
|> result.map(fn(content) {
content
|> string.split("\n")
|> list.map(string.trim_end)
})
}
fn split_module(
fn_name: String,
content: List(String),
) -> #(List(String), List(String), List(String), List(String)) {
let #(rest, epilogue) =
list.split_while(content, fn(line) {
!string.starts_with(line, "fn add_defaults(")
})
let #(before_function, rest) =
list.split_while(rest, fn(line) {
!string.starts_with(line, "pub fn " <> fn_name <> "(")
})
let #(function, after_function) =
list.split_while(rest, fn(line) {
!string.starts_with(line, "pub fn")
|| string.starts_with(line, "pub fn " <> fn_name <> "(")
})
#(before_function, function, after_function, epilogue)
}
fn normalize(fn_name: String) -> String {
let normalized_name =
fn_name
|> string.lowercase()
|> string.split("")
|> list.map(fn(char) {
case char {
"a"
| "b"
| "c"
| "d"
| "e"
| "f"
| "g"
| "h"
| "i"
| "j"
| "k"
| "l"
| "m"
| "n"
| "o"
| "p"
| "q"
| "r"
| "s"
| "t"
| "u"
| "v"
| "w"
| "x"
| "y"
| "z" -> char
_ -> "_"
}
})
|> string.join("")
case normalized_name {
"as"
| "assert"
| "auto"
| "case"
| "delegate"
| "derive"
| "echo"
| "else"
| "fn"
| "if"
| "implement"
| "import"
| "let"
| "macro"
| "opaque"
| "panic"
| "pub"
| "test"
| "todo"
| "type"
| "use" -> normalized_name <> "_"
_ -> normalized_name
}
}