Skip to content

Commit

Permalink
Move packaging to glimpse main module and increase error context
Browse files Browse the repository at this point in the history
  • Loading branch information
dusty-phillips committed Sep 9, 2024
1 parent 7e30d8b commit c23042c
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 125 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ This package is not filesystem aware. That means all modules need to be loaded
externally, but it does provide tooling to determine what dependencies need
to be loaded.

Docs: https://hexdocs.pm/glimpse/
Repo: https://github.com/dusty-phillips/glimpse

## Development

```sh
Expand Down
6 changes: 3 additions & 3 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name = "glimpse"
version = "0.0.1"
version = "0.0.2"

description = "Package loader and (theoretically) typechecker for glance"
licences = ["Apache-2.0"]
repository = { type = "github", user = "dustyphillips", repo = "glimpse" }
repository = { type = "github", user = "dusty-phillips", repo = "glimpse" }
links = [
{ title = "Website", href = "https://github.com/dustyphillips/glimpse" },
{ title = "Website", href = "https://github.com/dusty-phillips/glimpse" },
]
#
# For a full reference of all the available options, you can have a look at
Expand Down
102 changes: 102 additions & 0 deletions src/glimpse.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import glance
import gleam/dict
import gleam/list
import gleam/result

pub type Module {
/// A Module is a wrapper of a glance.Module, but maintains some additional
/// information such as the name of the package and the names of any dependencies
/// of that module.
Module(name: String, module: glance.Module, dependencies: List(String))
}

pub type Package {
/// A Package has a name and a collection of modules. Each module is named
/// according to its import. A module can be converted to a filename
///(relative to the global source / directory) simply by appending '.gleam'
Package(name: String, modules: dict.Dict(String, Module))
}

pub type LoadError(a) {
LoadError(a)
ParseError(
glance_error: glance.Error,
module_name: String,
module_content: String,
)
}

/// Load a package using just it's name and a loader function. The name is
/// required to be identical to the entry point module for the package (which
/// is normal in gleam packages).
///
/// Recursively loads all imports in the entrypoint using the supplied
/// loader function.
pub fn load_package(
package_name: String,
loader: fn(String) -> Result(String, a),
) -> Result(Package, LoadError(a)) {
let package = Package(package_name, dict.new())
load_module_recurse(package, [package_name], loader)
}

/// Given an existing Gleam Module and its name, parse its imports to determine
/// what its dependencies are. Return a Glimpse Module that wraps the Gleam module.
///
/// Note: If the returned Module has any dependencies, it is up to the caller to ensure
/// they are loaded, along with their dependencies.
pub fn load_module(module: glance.Module, name: String) -> Module {
module.imports
|> list.map(fn(import_def) {
case import_def {
glance.Definition(definition: import_, ..) -> import_.module
}
})
|> Module(name, module, _)
}

/// given a module and package, identify any dependencies in the module that
/// are *not* present in the package, and return them as a list.
///
/// The dependencies will be a list of strings like `gleam/io` or `glance`
pub fn filter_new_dependencies(module: Module, package: Package) -> List(String) {
module.dependencies
|> list.filter(fn(dep) { !dict.has_key(package.modules, dep) })
}

fn load_module_recurse(
package: Package,
modules: List(String),
loader: fn(String) -> Result(String, a),
) -> Result(Package, LoadError(a)) {
case modules {
[] -> Ok(package)
[module_name, ..rest] -> {
case dict.has_key(package.modules, module_name) {
True -> load_module_recurse(package, rest, loader)
False -> {
use module_content <- result.try(
loader(module_name) |> result.map_error(LoadError),
)
use glance_module <- result.try(
glance.module(module_content)
|> result.map_error(ParseError(_, module_name, module_content)),
)
let glimpse_module = load_module(glance_module, module_name)
let unknown_dependencies =
filter_new_dependencies(glimpse_module, package)
let recurse_package =
Package(
..package,
modules: dict.insert(package.modules, module_name, glimpse_module),
)
load_module_recurse(
recurse_package,
list.append(unknown_dependencies, rest),
loader,
)
}
}
}
}
}
97 changes: 0 additions & 97 deletions src/glimpse/package.gleam

This file was deleted.

30 changes: 15 additions & 15 deletions test/filter_new_dependencies_test.gleam
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
import glance
import gleam/dict
import gleeunit/should
import glimpse/package
import glimpse

fn empty_module(name: String) -> package.Module {
fn empty_module(name: String) -> glimpse.Module {
glance.module("")
|> should.be_ok
|> package.Module(name, _, [])
|> glimpse.Module(name, _, [])
}

pub fn no_dependencies_test() {
let module =
glance.module("")
|> should.be_ok
|> package.load_module("some/module")
|> glimpse.load_module("some/module")

let package = package.Package("some_package", dict.new())
let package = glimpse.Package("some_package", dict.new())

package.filter_new_dependencies(module, package)
glimpse.filter_new_dependencies(module, package)
|> should.equal([])
}

pub fn new_dependency_test() {
let module =
glance.module("import gleam/io")
|> should.be_ok
|> package.load_module("some/module")
|> glimpse.load_module("some/module")

let package = package.Package("some_package", dict.new())
let package = glimpse.Package("some_package", dict.new())

package.filter_new_dependencies(module, package)
glimpse.filter_new_dependencies(module, package)
|> should.equal(["gleam/io"])
}

pub fn old_dependency_test() {
let module =
glance.module("import gleam/io")
|> should.be_ok
|> package.load_module("some/module")
|> glimpse.load_module("some/module")

let package =
package.Package(
glimpse.Package(
"some_package",
dict.new() |> dict.insert("gleam/io", empty_module("gleam/io")),
)

package.filter_new_dependencies(module, package)
glimpse.filter_new_dependencies(module, package)
|> should.equal([])
}

Expand All @@ -56,14 +56,14 @@ pub fn one_old_one_new_dependency_test() {
import gleam/list",
)
|> should.be_ok
|> package.load_module("some/module")
|> glimpse.load_module("some/module")

let package =
package.Package(
glimpse.Package(
"some_package",
dict.new() |> dict.insert("gleam/io", empty_module("gleam/io")),
)

package.filter_new_dependencies(module, package)
glimpse.filter_new_dependencies(module, package)
|> should.equal(["gleam/list"])
}
6 changes: 3 additions & 3 deletions test/load_module_test.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import glance
import gleeunit/should
import glimpse/package
import glimpse

pub fn zero_dependency_test() {
let glance_module =
Expand All @@ -10,7 +10,7 @@ pub fn zero_dependency_test() {

let glimpse_module =
glance_module
|> package.load_module("some/module")
|> glimpse.load_module("some/module")

should.equal(glimpse_module.name, "some/module")
should.equal(glimpse_module.dependencies, [])
Expand All @@ -27,7 +27,7 @@ pub fn unqualified_import_test() {

let glimpse_module =
glance_module
|> package.load_module("some/module")
|> glimpse.load_module("some/module")

should.equal(glimpse_module.name, "some/module")
should.equal(glimpse_module.dependencies, ["gleam/io"])
Expand Down
20 changes: 13 additions & 7 deletions test/load_package_test.gleam
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import glance
import gleam/dict
import gleeunit/should
import glimpse/package
import glimpse

pub fn ok_module(contents: String) -> glance.Module {
glance.module(contents)
|> should.be_ok
}

pub fn no_dependency_package_test() {
package.load_package("main_module", fn(_) { Ok("pub fn main() {}") })
glimpse.load_package("main_module", fn(_) { Ok("pub fn main() {}") })
|> should.be_ok
|> should.equal(package.Package(
|> should.equal(glimpse.Package(
"main_module",
dict.from_list([
#(
"main_module",
package.Module("main_module", ok_module("pub fn main() {}"), []),
glimpse.Module("main_module", ok_module("pub fn main() {}"), []),
),
]),
))
}

pub fn single_dependency_package_test() {
let loaded_package =
package.load_package("main_module", fn(mod) {
glimpse.load_package("main_module", fn(mod) {
case mod {
"main_module" ->
Ok(
Expand Down Expand Up @@ -57,7 +57,7 @@ pub fn single_dependency_package_test() {

pub fn diamond_dependency_package_test() {
let loaded_package =
package.load_package("main_module", fn(mod) {
glimpse.load_package("main_module", fn(mod) {
case mod {
"main_module" -> Ok("import a\nimport b")
"a" | "b" -> Ok("import gleam/io")
Expand Down Expand Up @@ -87,8 +87,14 @@ import b",
expect_modules_equal(loaded_package, "gleam/io", [], "")
}

pub fn loader_error_test() {
glimpse.load_package("main_module", fn(_mod) { Error("I am error") })
|> should.be_error
|> should.equal(glimpse.LoadError("I am error"))
}

fn expect_modules_equal(
package: package.Package,
package: glimpse.Package,
name: String,
expected_dependencies: List(String),
expected_module_contents: String,
Expand Down

0 comments on commit c23042c

Please sign in to comment.