package wtr

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file wtr_ppx.ml

1
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
(*-------------------------------------------------------------------------
 * Copyright (c) 2021 Bikal Gurung. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License,  v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *
 *-------------------------------------------------------------------------*)

open Ppxlib
module Ast_builder = Ast_builder.Default

let ( let* ) r f = Result.bind r f
let ( >>= ) = ( let* )

let rec make_route ~loc ~path:_ wtr =
  let wtr = String.trim wtr in
  let methods, uri =
    let tokens =
      String.split_on_char ';' wtr
      |> List.map String.trim
      |> List.filter (fun s -> not (String.equal "" s))
    in
    if List.length tokens != 2 then
      Location.raise_errorf ~loc
        "Invalid wtr: %s. Valid wtr is: [HTTP methods separated by comma (,)] \
         ; [URI]"
        wtr
    else (List.nth tokens 0, List.nth tokens 1)
  in
  (let* uri = parse_uri uri in
   let* query_components = parse_query_tokens uri in
   let* path_components = parse_path_tokens uri in
   validate_tokens (path_components @ query_components) )
  |> function
  | Ok uri_tokens ->
      let methods' = to_methods methods in
      let uris =
        ( if List.length methods' = 0 then [uri_tokens]
        else List.map (fun m -> m :: uri_tokens) methods' )
        |> make_uris ~loc
      in
      [%expr Wtr.Private.route [%e uris]]
  | Error msg -> Location.raise_errorf ~loc "wtr: %s" msg

and make_uris ~loc = function
  | [] -> [%expr []]
  | uri_toks :: l ->
      [%expr [%e make_uri ~loc uri_toks] :: [%e make_uris ~loc l]]

and to_methods methods_str =
  String.split_on_char ',' methods_str
  |> List.filter_map (fun s ->
         let s = String.trim s in
         if String.length s > 0 then Some ("^^" ^ String.uppercase_ascii s)
         else None )

and parse_uri wtr =
  let wtr = String.trim wtr in
  if String.length wtr > 0 then Ok (Uri.of_string wtr)
  else Error "Empty uri path specification"

and parse_query_tokens uri =
  let exception E of string in
  try
    Uri.query uri
    |> List.map (fun (k, v) ->
           if List.length v != 1 then
             raise
               (E (Printf.sprintf "Invalid query specification for key: %s" k))
           else [k; List.hd v] )
    |> List.concat |> Result.ok
  with E msg -> Error msg

and parse_path_tokens uri = Ok (Uri.path uri |> String.split_on_char '/')

and validate_tokens tokens =
  let validate_start tokens =
    match List.hd tokens with
    | "" -> Ok (List.tl tokens)
    | _ | (exception _) -> Error "Uri path specification must start with '/'"
  in
  let validate_end_slash path =
    let _, l2 = split_on (fun x -> String.equal "" x) path in
    if List.length l2 > 0 then
      Error
        "Invalid uri path specification. No tokens allowed after trailing '/' \
         token"
    else Ok path
  in
  let validate_full_splat path =
    let _, l2 = split_on (fun x -> String.equal "**" x) path in
    if List.length l2 > 0 then
      Error
        "Invalid uri path specification. No tokens allowed after full splat \
         (**) token"
    else Ok path
  in
  validate_start tokens >>= validate_end_slash >>= validate_full_splat

and findi f l =
  let rec loop n = function
    | [] -> None
    | x :: t -> if f x then Some n else loop (n + 1) t
  in
  loop 0 l

and starts_with ~prefix s =
  let len_s = String.length s and len_pre = String.length prefix in
  let rec aux i =
    if i = len_pre then true
    else if String.unsafe_get s i <> String.unsafe_get prefix i then false
    else aux (i + 1)
  in
  len_s >= len_pre && aux 0

and split_on f l =
  match findi f l with
  | Some n ->
      (List.filteri (fun i _ -> i < n) l, List.filteri (fun i _ -> i > n) l)
  | None -> (l, [])

and make_uri ~loc = function
  | [] -> [%expr Wtr.Private.nil]
  | [""] -> [%expr Wtr.Private.trailing_slash]
  | ["**"] -> [%expr Wtr.Private.full_splat]
  | "*" :: components ->
      [%expr
        Wtr.Private.decoder Wtr.Private.string [%e make_uri ~loc components]]
  | comp :: components when Char.equal comp.[0] ':' -> (
      (* Decoders *)
      let comp = String.sub comp 1 (String.length comp - 1) in
      match comp with
      | "int" ->
          [%expr
            Wtr.Private.decoder Wtr.Private.int [%e make_uri ~loc components]]
      | "int32" ->
          [%expr
            Wtr.Private.decoder Wtr.Private.int32 [%e make_uri ~loc components]]
      | "int64" ->
          [%expr
            Wtr.Private.decoder Wtr.Private.int64 [%e make_uri ~loc components]]
      | "float" ->
          [%expr
            Wtr.Private.decoder Wtr.Private.float [%e make_uri ~loc components]]
      | "string" ->
          [%expr
            Wtr.Private.decoder Wtr.Private.string [%e make_uri ~loc components]]
      | "bool" ->
          [%expr
            Wtr.Private.decoder Wtr.Private.bool [%e make_uri ~loc components]]
      | custom_arg when capitalized custom_arg ->
          let longident_loc = {txt= Longident.parse (custom_arg ^ ".t"); loc} in
          [%expr
            Wtr.Private.decoder
              [%e Ast_builder.pexp_ident ~loc longident_loc]
              [%e make_uri ~loc components]]
      | x ->
          Location.raise_errorf ~loc
            "wtr: Invalid custom argument name '%s'. Custom argument component \
             name must be a valid module name."
            x )
  | comp :: components when starts_with ~prefix:"^^" comp ->
      (* Methods *)
      let method' = String.(sub comp 2 (length comp - 2)) in
      let meth_expr =
        [%expr Wtr.method' [%e Ast_builder.estring ~loc method']]
      in
      [%expr Wtr.Private.method' [%e meth_expr] [%e make_uri ~loc components]]
  | comp :: components ->
      [%expr
        Wtr.Private.lit
          [%e Ast_builder.estring ~loc comp]
          [%e make_uri ~loc components]]

and capitalized s = Char.(uppercase_ascii s.[0] |> equal s.[0])

let ppx_name = "wtr"

let ext =
  Extension.declare ppx_name Extension.Context.Expression
    Ast_pattern.(single_expr_payload (estring __))
    make_route

let () = Driver.register_transformation ppx_name ~extensions:[ext]
OCaml

Innovation. Community. Security.