src/gleedoc.gleam

import gleam/list
import gleam/result
import gleam/string
import gleedoc/extract
import gleedoc/generate.{Config}
import gleedoc/parse
import simplifile
import snag

/// Configuration for a gleedoc run.
pub type GleedocConfig {
  GleedocConfig(
    /// Directory to write generated tests to, typically "test"
    output_dir: String,
    /// Directory to read source files from, typically "src"
    source_dir: String,
  )
}

/// Run gleedoc on a project, extracting doc tests from source files and generating
/// test files in the output directory.
pub fn run(config: GleedocConfig) -> Result(Nil, snag.Snag) {
  // Find all gleam source files
  use files <- result.try(find_gleam_files(config.source_dir))

  // Extract doc blocks from all files
  use doc_blocks <- result.try(
    files
    |> list.try_map(extract.doc_blocks_from_file)
    |> result.map(list.flatten),
  )

  // Extract gleam code blocks from doc comments
  let code_blocks =
    doc_blocks
    |> parse.extract_code_blocks
    |> parse.gleam_blocks

  case code_blocks {
    [] -> {
      // Nothing to do
      Ok(Nil)
    }
    blocks -> {
      let gen_config = Config(output_dir: config.output_dir)

      // Clean old generated tests first
      use _ <- result.try(generate.clean_generated(config.output_dir))

      // Generate new test files
      use _ <- result.try(generate.generate_tests(blocks, gen_config))

      Ok(Nil)
    }
  }
}

/// CLI entry point. Reads gleam.toml to infer module/package names.
pub fn main() -> Nil {
  let config = GleedocConfig(output_dir: "test", source_dir: "src")

  case run(config) {
    Ok(Nil) -> Nil
    Error(snag) -> panic as snag.issue
  }
}

fn find_gleam_files(source_dir: String) -> Result(List(String), snag.Snag) {
  go_find_gleam_files(source_dir, [])
}

fn go_find_gleam_files(
  dir: String,
  acc: List(String),
) -> Result(List(String), snag.Snag) {
  use entries <- result.try(
    dir
    |> simplifile.read_directory
    |> result.map_error(fn(err) {
      snag.new(
        "Failed to read directory: " <> dir <> " - " <> string.inspect(err),
      )
    }),
  )

  entries
  |> list.try_fold(acc, fn(acc, entry) {
    let path = dir <> "/" <> entry
    use is_dir <- result.try(
      path
      |> simplifile.is_directory
      |> result.map_error(fn(err) {
        snag.new("Failed to stat: " <> path <> " - " <> string.inspect(err))
      }),
    )

    case is_dir {
      True -> go_find_gleam_files(path, acc)
      False -> {
        case string.ends_with(path, ".gleam") {
          True -> Ok([path, ..acc])
          False -> Ok(acc)
        }
      }
    }
  })
}