Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
path_template.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167type segment = | Static of string | Param of string type t = { raw : string; segments : segment list; } let raw template = template.raw let segments template = template.segments let route_error ?got message = Error (Error.make ?got ~location:Error.Route message) let path_param_error name ?expected ?got message = Error (Error.make ?expected ?got ~location:(Error.Path_param name) message) let split_path path = if String.length path = 0 || path.[0] <> '/' then Error (Error.make ~location:Error.Route ~got:path "path must start with /") else match String.split_on_char '/' path with | "" :: [ "" ] -> Ok [] | "" :: parts when List.exists (( = ) "") parts -> Error (Error.make ~location:Error.Route ~got:path "empty path segment") | "" :: parts -> Ok parts | _ -> Error (Error.make ~location:Error.Route ~got:path "empty path segment") let parse_segment = function | "" -> route_error "empty path segment" | segment when segment.[0] = ':' -> let name = String.sub segment 1 (String.length segment - 1) in if name = "" then route_error "empty path parameter" else Ok (Param name) | segment -> Ok (Static segment) let parse raw = match split_path raw with | Error error -> Error error | Ok parts -> let rec parse_all acc = function | [] -> Ok { raw; segments = List.rev acc } | part :: rest -> ( match parse_segment part with | Ok segment -> parse_all (segment :: acc) rest | Error error -> Error error) in parse_all [] parts let is_hex = function | '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true | _ -> false let hex_value = function | '0' .. '9' as ch -> Char.code ch - Char.code '0' | 'a' .. 'f' as ch -> 10 + Char.code ch - Char.code 'a' | 'A' .. 'F' as ch -> 10 + Char.code ch - Char.code 'A' | _ -> invalid_arg "hex_value" let is_valid_utf8 value = let length = String.length value in let byte index = Char.code value.[index] in let byte_in_range index low high = index < length && let value = byte index in value >= low && value <= high in let continuation index = byte_in_range index 0x80 0xbf in let rec valid_at index = if index = length then true else let first = byte index in if first <= 0x7f then valid_at (index + 1) else if first >= 0xc2 && first <= 0xdf then continuation (index + 1) && valid_at (index + 2) else if first = 0xe0 then byte_in_range (index + 1) 0xa0 0xbf && continuation (index + 2) && valid_at (index + 3) else if first >= 0xe1 && first <= 0xec then continuation (index + 1) && continuation (index + 2) && valid_at (index + 3) else if first = 0xed then byte_in_range (index + 1) 0x80 0x9f && continuation (index + 2) && valid_at (index + 3) else if first >= 0xee && first <= 0xef then continuation (index + 1) && continuation (index + 2) && valid_at (index + 3) else if first = 0xf0 then byte_in_range (index + 1) 0x90 0xbf && continuation (index + 2) && continuation (index + 3) && valid_at (index + 4) else if first >= 0xf1 && first <= 0xf3 then continuation (index + 1) && continuation (index + 2) && continuation (index + 3) && valid_at (index + 4) else if first = 0xf4 then byte_in_range (index + 1) 0x80 0x8f && continuation (index + 2) && continuation (index + 3) && valid_at (index + 4) else false in valid_at 0 let percent_decode_param name value = let length = String.length value in let buffer = Buffer.create length in let rec decode_at index = if index = length then let decoded = Buffer.contents buffer in if is_valid_utf8 decoded then Ok decoded else path_param_error name ~expected:"valid UTF-8" ~got:value "invalid UTF-8 path parameter" else match value.[index] with | '%' -> if index + 2 >= length || (not (is_hex value.[index + 1])) || not (is_hex value.[index + 2]) then path_param_error name ~expected:"%HH" ~got:value "malformed percent escape" else ( let code = (hex_value value.[index + 1] * 16) + hex_value value.[index + 2] in Buffer.add_char buffer (Char.chr code); decode_at (index + 3)) | ch -> Buffer.add_char buffer ch; decode_at (index + 1) in decode_at 0 let match_path template path = let rec match_segments params segments parts = match (segments, parts) with | [], [] -> Ok (List.rev params) | Static expected :: rest_segments, got :: rest_parts when String.equal expected got -> match_segments params rest_segments rest_parts | Param name :: rest_segments, got :: rest_parts -> ( match percent_decode_param name got with | Ok value -> match_segments ((name, value) :: params) rest_segments rest_parts | Error error -> Error error) | _ -> route_error ~got:path "path does not match route" in match split_path path with | Error error -> Error error | Ok parts -> match_segments [] template.segments parts let segment_to_openapi = function | Static segment -> segment | Param name -> "{" ^ name ^ "}" let to_openapi_path template = "/" ^ String.concat "/" (List.map segment_to_openapi template.segments)