Current section
Files
Jump to
Current section
Files
src/howdy/url_parser.gleam
//// Handles matching of router template to the incoming request route
import gleam/uri
import gleam/list
import gleam/result
import gleam/option.{None, Option, Some, from_result}
import gleam/dynamic.{Dynamic}
import gleam/string
import gleam/int
import gleam/float
import howdy/uuid
/// Error return types for parsing URL segments
pub type SegmentError {
/// None matching URL
NonMatchingUrl
}
/// Matching segment types
pub type UrlSegment {
/// Set when the template segment matches the request segment
MatchingSegment(lhs: String, rhs: String)
/// Sets a template segemnt if the URL matches the templated segment
MatchingTemplateTypedSegment(
lhs: String,
rhs: String,
name: String,
value: dynamic.Dynamic,
)
/// Sets the wildcard segment if the template ends with a '*'
///
/// ### Example
///
/// ```gleam
/// WildcardSegment(lhs: "/blog/*", rhs: "/blog/page1/content", wildcard_path: "page1/content")
/// ```
WildcardSegment(lhs: String, rhs: String, wilcard_path: String)
/// Sets a non matching segment, this means that the Url from the
/// request no longer matches the template url, and forces the parse
/// to stop parsing the remaining url segments and retur an Error
NonMatchingSegment
}
/// parses the route template and the request path into a list of UrlSegments
pub fn parse(template: String, path: String) {
case string.ends_with(template, "*") {
True -> wildcard_parse(template, path)
False -> template_parse(template, path)
}
}
fn template_parse(template: String, path: String) {
let template_url = uri.path_segments(template)
let path_url = uri.path_segments(path)
list.strict_zip(template_url, path_url)
|> result.replace_error(NonMatchingUrl)
|> result.map(url_parts)
|> result.flatten
}
fn wildcard_parse(template: String, path: String) {
let template_url = uri.path_segments(template)
let path_url = uri.path_segments(path)
list.zip(template_url, path_url)
|> wildcard_parts(template, path)
}
fn wildcard_parts(
url_parts: List(#(String, String)),
template: String,
path: String,
) {
list.map(
url_parts,
fn(part) {
case is_wild_card(part.0), template_matches_path(part) {
True, _ ->
WildcardSegment(part.0, part.1, wildcard_join_paths(template, path))
_, True -> MatchingSegment(part.0, part.1)
_, _ -> NonMatchingSegment
}
},
)
|> has_nonmatching_segments
}
fn wildcard_join_paths(template: String, path: String) {
let striped_template = string.drop_right(template, 1)
string.replace(path, striped_template, "")
}
fn is_wild_card(template_segment: String) {
template_segment == "*"
}
fn url_parts(url_parts: List(#(String, String))) {
list.map(url_parts, convert_to_url_segment)
|> has_nonmatching_segments
}
// This is a problem, it can be optimised once if is introduced, see
// https://github.com/gleam-lang/suggestions/issues/68
// for more details
fn convert_to_url_segment(url_part: #(String, String)) {
case is_templated_path(url_part), template_matches_path(url_part) {
True, False -> process_templated_url_segment(url_part)
False, True -> MatchingSegment(url_part.0, url_part.1)
_, _ -> NonMatchingSegment
}
}
// fn convert_to_url_segment(url_part: #(String, String)) {
// case url_part {
// #(template, value) if is_templated_path(template) ->
// process_templated_url_segment(url_part)
// #(template, value) if template == value -> MatchingSegment(template, value)
// _ -> NonMatchingSegment
// }
// }
fn is_templated_path(key_value: #(String, String)) {
string.starts_with(key_value.0, "{") && string.ends_with(key_value.0, "}")
}
fn template_matches_path(key_value: #(String, String)) {
string.lowercase(key_value.0) == string.lowercase(key_value.1)
}
fn process_templated_url_segment(url_part: #(String, String)) {
case do_process_templated_url_segment(url_part) {
Ok(result) -> result
Error(_) -> NonMatchingSegment
}
}
fn do_process_templated_url_segment(url_part: #(String, String)) {
let #(template_url, path_url) = url_part
template_url
|> string.drop_left(up_to: 1)
|> string.drop_right(up_to: 1)
|> string.split_once(on: ":")
|> result.map(process_template(_, path_url))
|> result.map(convert_to_template_url_segment(_, template_url, path_url))
}
fn process_template(
key_value: #(String, String),
input: String,
) -> #(Option(Dynamic), String) {
let #(name, template_type) = key_value
let result = case string.lowercase(template_type) {
"string" -> process_string(input)
"int" -> process_int(input)
"float" -> process_float(input)
"uuid" -> process_uuid(input)
_ -> None
}
#(result, name)
}
fn convert_to_template_url_segment(
result: #(Option(Dynamic), String),
template_segment: String,
path_segment: String,
) {
let #(processed, name) = result
case processed {
Some(value) ->
MatchingTemplateTypedSegment(template_segment, path_segment, name, value)
None -> NonMatchingSegment
}
}
fn process_string(value: String) -> Option(Dynamic) {
string.to_option(value)
|> option.map(fn(str) { dynamic.from(str) })
}
fn process_int(value: String) -> Option(Dynamic) {
int.parse(value)
|> from_result()
|> option.map(fn(num) { dynamic.from(num) })
}
fn process_float(value: String) -> Option(Dynamic) {
float.parse(value)
|> from_result()
|> option.map(fn(num) { dynamic.from(num) })
}
fn process_uuid(value: String) -> Option(Dynamic) {
uuid.from_string(value)
|> from_result()
|> option.map(fn(_id) { dynamic.from(value) })
}
fn has_nonmatching_segments(segments: List(UrlSegment)) {
case list.contains(segments, NonMatchingSegment) {
True -> Error(NonMatchingUrl)
False -> Ok(segments)
}
}