Skip to content

Commit

Permalink
Finish local vs tmp project overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
sbergen committed Nov 10, 2024
1 parent 80a24bf commit a868a1e
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 168 deletions.
20 changes: 7 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,16 @@ but plan on doing so soon, after getting some feedback.

```gleam
import checkmark
import filepath
import gleam/string
import simplifile
pub fn main() {
let assert Ok(cwd) = simplifile.current_directory()
let file = filepath.join(cwd, "README.md")
// Checks that a single gleam code block in README.md that starts with "import"
// passes type checks, adding "my_dependency" as a package.
let assert Ok([Ok(Nil)]) =
checkmark.check(
in: file,
using: ["my_dependency"],
selecting: string.starts_with(_, "import"),
operation: checkmark.Check,
)
// passes type checks, by creating the temporary file `checkmark_tmp.gleam`
// Checking in a temporary project is also supported.
let assert Ok([Ok(_)]) =
checkmark.new()
|> checkmark.snippets_in("README.md")
|> checkmark.filtering(string.starts_with(_, "import"))
|> checkmark.check_in_current_package("checkmark_tmp.gleam")
}
```
192 changes: 129 additions & 63 deletions src/checkmark.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import exception
import filepath
import gleam/io
import gleam/function
import gleam/list
import gleam/result.{try}
import gleam/string
Expand All @@ -16,28 +16,108 @@ pub type Operation {
}

pub type CheckError {
CouldNotRun(String)
RunFailed(String)
CheckFailed(String)
}

pub fn check(
in file: String,
using dependencies: List(String),
selecting filter: fn(String) -> Bool,
operation operation: Operation,
) -> Result(List(Result(Nil, CheckError)), String) {
use content <- try(
simplifile.read(from: file) |> result.map_error(string.inspect),
pub type CheckResult =
Result(Nil, CheckError)

pub opaque type CheckConfig {
CheckConfig(
cwd: String,
filename: String,
filter: fn(String) -> Bool,
operation: Operation,
)
}

/// Constructs a new check configuration with the defaults of
/// running `gleam check` on all snippets in the README.md file
pub fn new() -> CheckConfig {
let assert Ok(cwd) = simplifile.current_directory()
CheckConfig(cwd, "README.md", fn(_) { True }, Check)
}

pub fn snippets_in(config: CheckConfig, filename: String) -> CheckConfig {
CheckConfig(..config, filename: filename)
}

pub fn filtering(config: CheckConfig, filter: fn(String) -> Bool) -> CheckConfig {
CheckConfig(..config, filter: filter)
}

pub fn using(config: CheckConfig, operation: Operation) -> CheckConfig {
CheckConfig(..config, operation: operation)
}

/// Runs checks in the current package, writing into the given filename in src.
/// The result will be Error if the whole operation failed (e.g. couldn't read snippets)
/// or Ok with a list of results for each snippet found in the file.
pub fn check_in_current_package(
config: CheckConfig,
as_file filename: String,
) -> Result(List(CheckResult), String) {
use snippet <- check_snippets_in(config.filename, config.filter)
check_in_dir(snippet, ".", filename, False, config.operation)
}

/// Runs checks in a temporarily set up package, installing the given dependencies.
/// The result will be Error if the whole operation failed (e.g. couldn't read snippets)
/// or Ok with a list of results for each snippet found in the file.
pub fn check_in_tmp_package(
config: CheckConfig,
dependencies: List(String),
) -> Result(List(CheckResult), String) {
use temp_dir <- with_tempdir()
use package_dir <- try(set_up_package(temp_dir, dependencies))
use snippet <- check_snippets_in(config.filename, config.filter)
check_in_dir(
snippet,
package_dir,
package_name <> ".gleam",
True,
config.operation,
)
}

fn set_up_package(
tmp_dir: String,
dependencies: List(String),
) -> Result(String, String) {
use _ <- try(run_gleam(
in: tmp_dir,
with: ["new", package_name],
fail_as: function.identity,
))

let package_dir = filepath.join(tmp_dir, package_name)
use _ <- try(case dependencies {
[] -> Ok(Nil)
_ ->
run_gleam(
in: package_dir,
with: ["add", ..dependencies],
fail_as: function.identity,
)
})

Ok(package_dir)
}

Ok(
extract_gleam_code(content, filter)
|> list.map(check_code(_, dependencies, operation)),
fn check_snippets_in(
filename: String,
filter: fn(String) -> Bool,
check: fn(String) -> CheckResult,
) -> Result(List(CheckResult), String) {
use content <- try(
simplifile.read(from: filename) |> result.map_error(string.inspect),
)

Ok(extract_gleam_code(content, filter) |> list.map(check))
}

@internal
pub fn extract_gleam_code(
fn extract_gleam_code(
markdown: String,
filter: fn(String) -> Bool,
) -> List(String) {
Expand All @@ -56,91 +136,77 @@ pub fn extract_gleam_code(

const package_name = "checkmark_tmp"

@internal
pub fn check_code(
code: String,
dependencies: List(String),
operation: Operation,
) -> Result(Nil, CheckError) {
let file_result = {
use temp_dir <- temporary.create(temporary.directory())
let package_dir = filepath.join(temp_dir, package_name)

use _ <- try(run_gleam(
in: temp_dir,
with: ["new", package_name],
fail_as: CouldNotRun,
))

use _ <- try(case dependencies {
[] -> Ok(Nil)
_ ->
run_gleam(
in: package_dir,
with: ["add", ..dependencies],
fail_as: CouldNotRun,
)
})

check_in_dir(code, package_dir, operation)
}

case file_result {
Ok(r) -> r
Error(e) -> Error(CouldNotRun(string.inspect(e)))
}
}

fn check_in_dir(
code: String,
package_dir: String,
filename: String,
allow_overwrite: Bool,
operation: Operation,
) -> Result(Nil, CheckError) {
) -> CheckResult {
let source_dir = filepath.join(package_dir, "src")
let source_file = filepath.join(source_dir, package_name <> ".gleam")
use file <- with_tempfile(source_file, True)
let source_file = filepath.join(source_dir, filename)
use file <- with_tempfile(source_file, allow_overwrite)

use _ <- try(
simplifile.write(to: file, contents: code)
|> result.map_error(fn(e) { CouldNotRun(string.inspect(e)) }),
|> result.map_error(fn(e) { RunFailed(string.inspect(e)) }),
)

run_gleam(with: to_args(operation), in: package_dir, fail_as: CheckFailed)
case string.split(filename, ".") {
[] -> Error(CheckFailed("Invalid source file name: " <> filename))
[module, ..] ->
run_gleam(
with: to_args(operation, module),
in: package_dir,
fail_as: CheckFailed,
)
}
}

fn with_tempdir(
operation: fn(String) -> Result(List(CheckResult), String),
) -> Result(List(CheckResult), String) {
{
use temp_dir <- temporary.create(temporary.directory())
operation(temp_dir)
}
|> result.map_error(string.inspect)
|> result.flatten
}

fn with_tempfile(
path: String,
allow_overwrite: Bool,
operation: fn(String) -> Result(Nil, CheckError),
) {
operation: fn(String) -> CheckResult,
) -> CheckResult {
use _ <- try(create_file(path, allow_overwrite))
use <- exception.defer(fn() { simplifile.delete(path) })
operation(path)
}

fn create_file(path: String, allow_overwrite: Bool) -> Result(Nil, CheckError) {
fn create_file(path: String, allow_overwrite: Bool) -> CheckResult {
case allow_overwrite, simplifile.create_file(path) {
True, Error(simplifile.Eexist) -> Ok(Nil)
_, Ok(_) -> Ok(Nil)
_, e -> Error(CouldNotRun(string.inspect(e)))
_, e -> Error(RunFailed(string.inspect(e)))
}
}

fn run_gleam(
in directory: String,
with args: List(String),
fail_as make_error: fn(String) -> CheckError,
) -> Result(Nil, CheckError) {
fail_as make_error: fn(String) -> e,
) -> Result(Nil, e) {
case shellout.command("gleam", with: args, in: directory, opt: []) {
Ok(_) -> Ok(Nil)
Error(#(_, e)) -> Error(make_error(e))
}
}

fn to_args(op: Operation) -> List(String) {
fn to_args(op: Operation, module: String) -> List(String) {
case op {
Build -> ["build"]
Check -> ["check"]
Run -> ["run"]
Run -> ["run", "--module", module]
}
}
1 change: 1 addition & 0 deletions src/test_overwrite.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This file exists only to test that local files can't be overwritten
52 changes: 0 additions & 52 deletions test/checker_test.gleam

This file was deleted.

Loading

0 comments on commit a868a1e

Please sign in to comment.