class Analyzer::Gleam::Wisp

Overview

Wisp has no route table — routing is a case over the split path, and the verb comes either from a second subject in the same case or from the handler it delegates to. All four shapes are common in real apps:

case wisp.path_segments(req) {                  # path only
  ["users", id] -> user(req, id)
}

case wisp.path_segments(req), req.method {      # path, method
  ["api", "teams"], http.Post -> create_team(req)
}

case req.method, wisp.path_segments(req) {      # method, path
  Get, ["members"] -> members.list(req)
}

let path = wisp.path_segments(req)              # via let bindings
let method = req.method
case method, path {
  Get, [] -> home.render(req)
}

Rather than assume an order, the subject is parsed to learn which comma position holds the path and which holds the method, and each arm is then read against those positions. When the arm carries no verb the handler it calls is followed (resolve_handler) to find a case req.method or a wisp.require_method.

Defined in:

analyzer/analyzers/gleam/wisp.cr

Constant Summary

BODY_VERBS = Set {"POST", "PUT", "PATCH", "ANY"}

Verbs that can carry a request body.

CALL_REGEX = /(?:\b([a-z_][A-Za-z0-9_]*)\s*\.\s*)?\b([a-z_][A-Za-z0-9_]*)\s*\(/
CASE_KEYWORD = /\bcase\b/
COOKIE_REGEX = /wisp\.get_cookie\s*\(\s*[^,]+,\s*"([^"]+)"/
FORM_FIELD_REGEX = /key_find\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\.(?:values|files)\s*,\s*"([^"]+)"/

list.key_find(formdata.values, "title") — the binder is named formdata, form, … so match any receiver.

FORM_REGEX = /wisp\.require_form\s*\(/
FUNCTION_DEF = /^\s*(?:pub\s+)?fn\s+([a-z_][A-Za-z0-9_]*)\s*\(/
HEADER_REGEX = /\bget_header\s*\(\s*(?:[^,"()]+,\s*)?"([^"]+)"/

2-arg request.get_header(req, "accept") and the piped 1-arg |> request.get_header("accept") are both idiomatic.

HTTP_METHODS = {"Get" => "GET", "Post" => "POST", "Put" => "PUT", "Patch" => "PATCH", "Delete" => "DELETE", "Head" => "HEAD", "Options" => "OPTIONS", "Trace" => "TRACE", "Connect" => "CONNECT"}
IGNORED_CALL_MODULES = Set {"wisp", "gleam", "http", "request", "response", "io", "list", "string", "int", "float", "result", "option", "json", "dict", "bool", "bit_array", "dynamic", "decode", "uri", "bytes_tree", "string_tree", "mist", "case", "fn", "use", "let"}

Calls into the standard library and wisp itself are responses and helpers, never route handlers.

IMPORT_LINE = /^\s*import\s+([a-z_][A-Za-z0-9_\/]*)(?:\s*\.\s*\{[^}]*\})?(?:\s+as\s+([a-z_][A-Za-z0-9_]*))?/
JSON_FIELD_REGEX = /\bfield\s*\(\s*"([^"]+)"/

A wisp.require_json body is decoded elsewhere; decode.field is where the field names actually appear.

JSON_REGEX = /wisp\.require_json\s*\(/
MAX_RESOLVE_DEPTH = 3

Depth limit for following a route arm through handler functions to the case req.method that names its verb.

MAX_SUBJECT_LINES = 8

A case subject spans at most a handful of lines even when the formatter wraps it.

METHOD_BINDING = /\blet\s+([a-z_][A-Za-z0-9_]*)\s*=\s*[a-z_][A-Za-z0-9_]*\.method\b/
METHOD_CASE = /\bcase\s+[A-Za-z_][A-Za-z0-9_]*\.method\s*\{/
METHOD_SOURCE = /\.method\b/
METHOD_TOKEN = /\A(?:[a-z_][A-Za-z0-9_]*\.)?([A-Z][A-Za-z0-9_]*)\z/
NON_ROUTE_BODY = /\A\s*(?:wisp\.)?(?:method_not_allowed|not_found|bad_request|unprocessable_entity|internal_server_error|handle_request\s*\(\s*\))/

_ -> wisp.method_not_allowed([Get, Post]) and _, _ -> wisp.not_found() are the fallthrough arms every wisp router ends with. They match a path but serve no endpoint.

PATH_BINDING = /\blet\s+([a-z_][A-Za-z0-9_]*)\s*=[^\n]*\bpath_segments\s*\(/

let path = wisp.path_segments(req) / let method = req.method. Apps that bind these before the case would otherwise leave the subject looking like two bare identifiers.

PATH_SOURCE = /\bpath_segments\s*\(/
REQUIRE_METHOD = /require_method\s*\(\s*[^,)]+,\s*(?:[a-z_][A-Za-z0-9_]*\.)?([A-Z][A-Za-z0-9_]*)/
SERVE_STATIC = /wisp\.serve_static\s*\(/
STRING_BODY = /wisp\.require_(?:string|bit_array)_body\s*\(/

Instance Method Summary

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, callees_needed? : Bool callees_needed?, http_header_name(name : String) : String | Nil http_header_name, line_number_for_index(content : String, char_index : Int32) : Int32 line_number_for_index, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, unique_params(params : Array(Param)) : Array(Param) unique_params, url : String url, web_root_path(path : String, markers : Array(String)) : String web_root_path

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_extensions(extensions : Array(String)) : Array(String) get_files_by_extensions, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files

Instance Method Detail

def analyze #

[View source]