//// Command-line entrypoint for Gleamdoc.
import argv
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/package_interface
import gleam/result
import gleam/string
import gleamdoc/index
import gleamdoc/interface
import gleamdoc/output
import glint
import simplifile
import snag
const version = "0.2.0"
type Format {
Text
Json
}
type Scope {
Scope(package: Option(String), dependencies: Bool)
}
type Action {
Lookup(query: String, format: Format, scope: Scope)
Search(
query: String,
format: Format,
scope: Scope,
limit: Int,
kind: String,
target: String,
)
Packages(format: Format, dependencies: Bool)
Index(format: Format, scope: Scope)
}
/// Runs the Gleamdoc command-line application.
pub fn main() -> Nil {
case argv.load().arguments {
["--version"] | ["-v"] -> io.println("gleamdoc " <> version)
arguments ->
glint.run_and_handle(from: cli(), for: arguments, with: execute)
}
}
fn cli() -> glint.Glint(Action) {
glint.new()
|> glint.with_name("gleamdoc")
|> glint.global_help(
"Sparse, terminal-first documentation lookup for Gleam packages.",
)
|> glint.add(at: [], do: lookup_command())
|> glint.add(at: ["search"], do: search_command())
|> glint.add(at: ["packages"], do: packages_command())
|> glint.add(at: ["index"], do: index_command())
}
fn lookup_command() -> glint.Command(Action) {
use <- glint.command_help(
"Look up an exact public module, function, constant, type, variant, or alias.",
)
use query <- glint.named_arg("query")
use json <- glint.flag(json_flag())
use package <- glint.flag(package_flag())
use no_deps <- glint.flag(no_deps_flag())
use <- glint.unnamed_args(glint.EqArgs(0))
use named, _, flags <- glint.command()
let assert Ok(json) = json(flags)
let assert Ok(package) = package(flags)
let assert Ok(no_deps) = no_deps(flags)
Lookup(
query(named),
output_format(json),
Scope(package: optional_package(package), dependencies: !no_deps),
)
}
fn search_command() -> glint.Command(Action) {
use <- glint.command_help(
"Search symbol names, modules, packages, signatures, and documentation.",
)
use json <- glint.flag(json_flag())
use limit <- glint.flag(limit_flag())
use kind <- glint.flag(kind_flag())
use target <- glint.flag(target_flag())
use package <- glint.flag(package_flag())
use no_deps <- glint.flag(no_deps_flag())
use <- glint.unnamed_args(glint.MinArgs(1))
use _, terms, flags <- glint.command()
let assert Ok(json) = json(flags)
let assert Ok(limit) = limit(flags)
let assert Ok(kind) = kind(flags)
let assert Ok(target) = target(flags)
let assert Ok(package) = package(flags)
let assert Ok(no_deps) = no_deps(flags)
Search(
string.join(terms, with: " "),
output_format(json),
Scope(package: optional_package(package), dependencies: !no_deps),
limit,
kind,
target,
)
}
fn packages_command() -> glint.Command(Action) {
use <- glint.command_help("List indexed packages and module counts.")
use json <- glint.flag(json_flag())
use no_deps <- glint.flag(no_deps_flag())
use <- glint.unnamed_args(glint.EqArgs(0))
use _, _, flags <- glint.command()
let assert Ok(json) = json(flags)
let assert Ok(no_deps) = no_deps(flags)
Packages(output_format(json), dependencies: !no_deps)
}
fn index_command() -> glint.Command(Action) {
use <- glint.command_help(
"Generate package-interface caches used by lookup and search commands.",
)
use json <- glint.flag(json_flag())
use package <- glint.flag(package_flag())
use no_deps <- glint.flag(no_deps_flag())
use <- glint.unnamed_args(glint.EqArgs(0))
use _, _, flags <- glint.command()
let assert Ok(json) = json(flags)
let assert Ok(package) = package(flags)
let assert Ok(no_deps) = no_deps(flags)
Index(
output_format(json),
Scope(package: optional_package(package), dependencies: !no_deps),
)
}
fn json_flag() -> glint.Flag(Bool) {
glint.bool_flag("json")
|> glint.flag_default(False)
|> glint.flag_help("Emit machine-readable JSON.")
}
fn package_flag() -> glint.Flag(String) {
glint.string_flag("package")
|> glint.flag_default("")
|> glint.flag_help("Restrict lookup or search to one package.")
}
fn no_deps_flag() -> glint.Flag(Bool) {
glint.bool_flag("no-deps")
|> glint.flag_default(False)
|> glint.flag_help("Only inspect the current project.")
}
fn limit_flag() -> glint.Flag(Int) {
glint.int_flag("limit")
|> glint.flag_default(50)
|> glint.flag_constraint(fn(limit) {
case limit > 0 {
True -> Ok(limit)
False -> snag.error("--limit must be greater than zero")
}
})
|> glint.flag_help("Maximum number of search results.")
}
fn kind_flag() -> glint.Flag(String) {
glint.string_flag("kind")
|> glint.flag_default("")
|> glint.flag_constraint(fn(kind) {
case
list.contains(
["", "module", "fn", "const", "type", "variant", "alias"],
kind,
)
{
True -> Ok(kind)
False ->
snag.error("--kind must be module, fn, const, type, variant, or alias")
}
})
|> glint.flag_help("Restrict search to one symbol kind.")
}
fn target_flag() -> glint.Flag(String) {
glint.string_flag("target")
|> glint.flag_default("")
|> glint.flag_constraint(fn(target) {
case list.contains(["", "erlang", "javascript"], target) {
True -> Ok(target)
False -> snag.error("--target must be erlang or javascript")
}
})
|> glint.flag_help("Restrict search to functions and constants for a target.")
}
fn output_format(json: Bool) -> Format {
case json {
True -> Json
False -> Text
}
}
fn optional_package(package: String) -> Option(String) {
case string.trim(package) {
"" -> None
package -> Some(package)
}
}
fn execute(action: Action) -> Nil {
let #(scope, refresh) = load_options(action)
case load_packages(scope, refresh) {
Error(message) -> fail(action_format(action), "error: " <> message)
Ok(all_packages) -> execute_with_packages(action, all_packages)
}
}
fn load_options(action: Action) -> #(Scope, Bool) {
case action {
Lookup(query, _, scope) -> #(lookup_load_scope(scope, query), False)
Search(_, _, scope, _, _, _) -> #(scope, False)
Packages(_, dependencies) -> #(
Scope(package: None, dependencies: dependencies),
False,
)
Index(_, scope) -> #(scope, True)
}
}
fn lookup_load_scope(scope: Scope, query: String) -> Scope {
let Scope(package:, dependencies:) = scope
case package, string.split_once(query, on: ":") {
None, Ok(#(package, _)) -> Scope(package: Some(package), dependencies:)
_, _ -> scope
}
}
fn action_format(action: Action) -> Format {
case action {
Lookup(_, format, _) -> format
Search(_, format, _, _, _, _) -> format
Packages(format, _) -> format
Index(format, _) -> format
}
}
fn execute_with_packages(
action: Action,
all_packages: List(package_interface.Package),
) -> Nil {
case action {
Lookup(query, format, scope) -> {
case scope_error(all_packages, scope) {
Some(message) -> fail(format, message)
None -> {
let packages = select_packages(all_packages, scope)
case interface.lookup_packages(packages, query) {
[] -> fail(format, "No public symbol found for `" <> query <> "`.")
entries ->
print(
format,
interface.render_all(entries),
interface.entries_json(entries),
output.Documentation,
)
}
}
}
}
Search(query, format, scope, limit, kind, target) -> {
case scope_error(all_packages, scope) {
Some(message) -> fail(format, message)
None -> {
let packages = select_packages(all_packages, scope)
case
interface.search_packages_filtered(
packages,
query,
limit,
kind: kind,
target: target,
)
{
[] -> fail(format, "No public symbols matched `" <> query <> "`.")
entries ->
print(
format,
interface.render_search_results(entries),
interface.entries_json(entries),
output.SearchResults,
)
}
}
}
}
Packages(format, dependencies) -> {
let packages = case dependencies {
True -> all_packages
False -> list.take(all_packages, 1)
}
print(
format,
interface.render_packages(packages),
interface.packages_json(packages),
output.Packages,
)
}
Index(format, _) -> {
let packages = all_packages
print(
format,
"Indexed "
<> int.to_string(list.length(packages))
<> " package interfaces.\n\n"
<> interface.render_packages(packages),
interface.packages_json(packages),
output.IndexResult,
)
}
}
}
fn scope_error(
packages: List(package_interface.Package),
scope: Scope,
) -> Option(String) {
let Scope(package:, ..) = scope
case package {
None -> None
Some(wanted) ->
case select_packages(packages, scope) {
[] -> Some("Package `" <> wanted <> "` is not indexed in this scope.")
_ -> None
}
}
}
fn select_packages(
packages: List(package_interface.Package),
scope: Scope,
) -> List(package_interface.Package) {
let Scope(package:, dependencies:) = scope
let packages = case dependencies {
True -> packages
False -> list.take(packages, 1)
}
case package {
None -> packages
Some(wanted) ->
packages
|> list.filter(fn(package) {
let package_interface.Package(name:, ..) = package
name == wanted
})
}
}
fn print(
format: Format,
text: String,
json_value: json.Json,
style: output.Style,
) -> Nil {
case format {
Text ->
case stdout_supports_color() {
True -> text |> output.text(style) |> io.println
False -> io.println(text)
}
Json -> json_value |> json.to_string |> io.println
}
}
fn fail(format: Format, message: String) -> Nil {
case format {
Text ->
case stderr_supports_color() {
True -> message |> output.error |> io.println_error
False -> io.println_error(message)
}
Json ->
json.object([
#("schema_version", json.int(1)),
#("error", json.string(message)),
#("results", json.preprocessed_array([])),
])
|> json.to_string
|> io.println
}
exit_status(1)
}
fn load_packages(
scope: Scope,
refresh: Bool,
) -> Result(List(package_interface.Package), String) {
let Scope(package:, dependencies:) = scope
use paths <- result.try(index.package_interface_paths(
dependencies,
option.unwrap(package, ""),
refresh,
))
load_package_files(paths, [])
}
fn load_package_files(
paths: List(String),
packages: List(package_interface.Package),
) -> Result(List(package_interface.Package), String) {
case paths {
[] -> Ok(list.reverse(packages))
[path, ..rest] -> {
use package <- result.try(load_package_file(path))
load_package_files(rest, [package, ..packages])
}
}
}
fn load_package_file(
path: String,
) -> Result(package_interface.Package, String) {
use source <- result.try(
simplifile.read(path)
|> result.map_error(fn(_) { "Could not read " <> path <> "." }),
)
json.parse(source, using: package_interface.decoder())
|> result.map_error(fn(_) {
"The generated package interface at " <> path <> " was invalid."
})
}
@external(erlang, "gleamdoc_ffi", "exit_status")
fn exit_status(status: Int) -> Nil
@external(erlang, "gleamdoc_ffi", "stdout_supports_color")
fn stdout_supports_color() -> Bool
@external(erlang, "gleamdoc_ffi", "stderr_supports_color")
fn stderr_supports_color() -> Bool