Current section

Files

Jump to
roundabout src roundabout.gleam
Raw

src/roundabout.gleam

import filepath
import glam/doc
import gleam/list
import gleam/result
import gleam/set
import gleam/string
import justin
import roundabout/internal/ancestors
import roundabout/internal/fixed
import roundabout/internal/generate_helpers
import roundabout/internal/generate_other
import roundabout/internal/generate_route_to_path
import roundabout/internal/generate_segments_to_route
import roundabout/internal/generate_types
import roundabout/internal/node
import roundabout/internal/parameter
import roundabout/internal/name
import simplifile
/// Path segments
pub opaque type Segment {
Fixed(value: String)
Str(name: String)
Int(name: String)
}
/// The route definition
pub opaque type Route {
Route(name: String, path: List(Segment), children: List(Route))
}
/// Make a route defintion
///
/// e.g.
/// ```gleam
/// route("users", [fixed("users"), int("user_id")], [])
/// ```
///
/// `name` is the name for this route, this will be used for generating the variant names and helper names.
///
/// `path` defines the url segments e.g. /users/1.
///
/// `children` generates sub types for nested routes, this is useful for middleware.
pub fn route(
name name: String,
path path: List(Segment),
children children: List(Route),
) {
Route(name:, path:, children:)
}
/// A path segment that is constant
///
/// e.g.
/// ```gleam
/// [fixed("users"), str("user_id")]
/// ```
pub fn fixed(value: String) {
Fixed(value)
}
/// A path segment that should resolve to a string
///
/// e.g.
/// ```gleam
/// [fixed("users"), str("user_id")]
/// ```
pub fn str(name: String) {
Str(name)
}
/// A path segment that should resolve to an integer
///
/// e.g.
/// ```gleam
/// [fixed("users"), int("user_id")]
/// ```
pub fn int(name: String) {
Int(name)
}
/// Generate the routes file
///
/// ```gleam
/// roundabout.main(route_definitions, "src/my_app/generated/routes")
/// ```
pub fn main(definitions: List(Route), output_path: String) {
use root <- result.try(parse(definitions))
let output_path = case string.ends_with(output_path, ".gleam") {
True -> output_path
False -> output_path <> ".gleam"
}
let types = generate_types.generate_type_rec(ancestors.empty(), root)
let segments_to_route =
generate_segments_to_route.generate_segments_to_route_rec(
ancestors.empty(),
root,
)
let routes_to_path =
generate_route_to_path.generate_route_to_path_rec(ancestors.empty(), root)
let helpers = generate_helpers.generate_helpers_rec(ancestors.empty(), root)
let utils = generate_other.generate_utils()
let all =
doc.concat([
generate_other.generate_header(),
generate_other.generate_imports(),
types,
segments_to_route,
routes_to_path,
helpers,
utils,
])
let generated_code = all |> doc.to_string(80)
let output_dir = filepath.directory_name(output_path)
let _ = simplifile.create_directory_all(output_dir)
let _ = simplifile.write(output_path, generated_code)
Ok(Nil)
}
@internal
pub fn parse(definitions: List(Route)) -> Result(node.Node, String) {
use children <- result.try(parse_definitions("root", definitions))
let root =
node.Node(children:, info: node.Info(name: name.unsafe(""), path: []))
Ok(root)
}
@internal
pub fn parse_definitions(
parent_name: String,
definitions: List(Route),
) -> Result(List(node.Node), String) {
use nodes <- result.try(list.try_map(definitions, parse_definition))
use nodes <- result.try(assert_no_duplicate_variant_names(parent_name, nodes))
Ok(nodes)
}
fn assert_no_duplicate_variant_names(
parent_name: String,
nodes: List(node.Node),
) {
let variant_names =
list.map(nodes, fn(item) { name.parameter_name(item.info.name) })
let as_set = set.from_list(variant_names)
case list.length(variant_names) == set.size(as_set) {
True -> Ok(nodes)
False -> Error("Route " <> parent_name <> " contains duplicate route names")
}
}
fn parse_definition(definition: Route) {
use info <- result.try(parse_definition_info(definition))
use children <- result.try(parse_definitions(
definition.name,
definition.children,
))
node.Node(info:, children:) |> Ok
}
fn parse_definition_info(input: Route) {
use input_path <- result.try(assert_no_duplicate_segment_names(
input.name,
input.path,
))
let path_result =
input_path
|> list.try_map(fn(seg) {
case seg {
Fixed(val) -> {
fixed.new(val)
|> result.map(node.SegFixed)
}
Str(val) -> {
parameter.new(val, parameter.Str)
|> result.map(node.SegParam)
}
Int(val) -> {
parameter.new(val, parameter.Int)
|> result.map(node.SegParam)
}
}
})
use name <- result.try(name.new(input.name))
use path <- result.try(path_result)
node.Info(name:, path:) |> Ok
}
fn assert_no_duplicate_segment_names(
node_name: String,
segments: List(Segment),
) {
let segment_names =
list.filter_map(segments, fn(seg) {
case seg {
Fixed(_) -> Error(Nil)
Str(val) -> Ok(justin.snake_case(val))
Int(val) -> Ok(justin.snake_case(val))
}
})
let as_set = set.from_list(segment_names)
case list.length(segment_names) == set.size(as_set) {
True -> Ok(segments)
False -> Error("Route " <> node_name <> " contains duplicate segment names")
}
}