package wtr-ppx

  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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
(*-------------------------------------------------------------------------
 * 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 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

let 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, [])

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

let validate_path_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_rest 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 rest \
         (**) token"
    else Ok path
  in
  validate_start tokens >>= validate_end_slash >>= validate_rest

let path_tokens uri =
  Uri.path uri |> String.split_on_char '/' |> validate_path_tokens

let 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))
    |> Result.ok
  with E msg -> Error msg

let request_target_tokens target =
  let target = String.trim target in
  if String.length target > 0 then
    let uri = Uri.of_string target in
    let* path_components = path_tokens uri in
    let* query_components = query_tokens uri in
    Ok (path_components, query_components)
  else Error "Empty uri path specification"

let make_methods : loc:location -> string -> expression =
 fun ~loc 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 s else None)
  |> List.rev
  |> List.fold_left
       (fun expr method' ->
         let method' = Ast_builder.estring ~loc method' in
         [%expr Wtr.method' [%e method'] :: [%e expr]])
       [%expr []]

let rec make_query ~loc query_tokens =
  match query_tokens with
  | [] -> [%expr Wtr.Private.nil]
  | (name, "*") :: query_tokens ->
      [%expr
        Wtr.Private.(
          query_arg
            [%e Ast_builder.estring ~loc name]
            string
            [%e make_query ~loc query_tokens])]
  | (name, query_token) :: query_tokens when Char.equal query_token.[0] ':' -> (
      let query_token =
        String.sub query_token 1 (String.length query_token - 1)
      in
      let name_expr = Ast_builder.estring ~loc name in
      match query_token with
      | "int" ->
          [%expr
            Wtr.Private.(
              query_arg [%e name_expr] int [%e make_query ~loc query_tokens])]
      | "int32" ->
          [%expr
            Wtr.Private.(
              query_arg [%e name_expr] int32 [%e make_query ~loc query_tokens])]
      | "int64" ->
          [%expr
            Wtr.Private.(
              query_arg [%e name_expr] int64 [%e make_query ~loc query_tokens])]
      | "float" ->
          [%expr
            Wtr.Private.(
              query_arg [%e name_expr] float [%e make_query ~loc query_tokens])]
      | "string" ->
          [%expr
            Wtr.Private.(
              query_arg [%e name_expr] string [%e make_query ~loc query_tokens])]
      | "bool" ->
          [%expr
            Wtr.Private.(
              query_arg [%e name_expr] bool [%e make_query ~loc query_tokens])]
      | custom_arg when capitalized custom_arg ->
          let longident_loc =
            { txt = Longident.parse (custom_arg ^ ".t"); loc }
          in
          [%expr
            Wtr.Private.query_arg [%e name_expr]
              [%e Ast_builder.pexp_ident ~loc longident_loc]
              [%e make_query ~loc query_tokens]]
      | x -> Location.raise_errorf ~loc "wtr: Invalid query component '%s'" x)
  | (name, query_token) :: query_tokens ->
      [%expr
        Wtr.Private.query_exact
          [%e Ast_builder.estring ~loc name]
          [%e Ast_builder.estring ~loc query_token]
          [%e make_query ~loc query_tokens]]

let rec make_request_target ~loc query_tokens path_tokens =
  match path_tokens with
  | [] -> make_query ~loc query_tokens
  | [ "" ] -> [%expr Wtr.Private.slash]
  | [ "**" ] -> [%expr Wtr.Private.rest]
  | "*" :: path_tokens ->
      [%expr
        Wtr.Private.(
          arg string [%e make_request_target ~loc query_tokens path_tokens])]
  | path_token :: path_tokens when Char.equal path_token.[0] ':' -> (
      let path_token = String.sub path_token 1 (String.length path_token - 1) in
      match path_token with
      | "int" ->
          [%expr
            Wtr.Private.(
              arg int [%e make_request_target ~loc query_tokens path_tokens])]
      | "int32" ->
          [%expr
            Wtr.Private.(
              arg int32 [%e make_request_target ~loc query_tokens path_tokens])]
      | "int64" ->
          [%expr
            Wtr.Private.(
              arg int64 [%e make_request_target ~loc query_tokens path_tokens])]
      | "float" ->
          [%expr
            Wtr.Private.(
              arg float [%e make_request_target ~loc query_tokens path_tokens])]
      | "string" ->
          [%expr
            Wtr.Private.(
              arg string [%e make_request_target ~loc query_tokens path_tokens])]
      | "bool" ->
          [%expr
            Wtr.Private.(
              arg bool [%e make_request_target ~loc query_tokens path_tokens])]
      | custom_arg when capitalized custom_arg ->
          let longident_loc =
            { txt = Longident.parse (custom_arg ^ ".t"); loc }
          in
          [%expr
            Wtr.Private.arg
              [%e Ast_builder.pexp_ident ~loc longident_loc]
              [%e make_request_target ~loc query_tokens path_tokens]]
      | x -> Location.raise_errorf ~loc "wtr: Invalid path component '%s'." x)
  | path_token :: path_tokens ->
      [%expr
        Wtr.Private.exact
          [%e Ast_builder.estring ~loc path_token]
          [%e make_request_target ~loc query_tokens path_tokens]]

let make_routes ~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
    let len = List.length tokens in
    if len > 2 then
      Location.raise_errorf ~loc
        "Invalid wtr: %s. Valid wtr is: [HTTP methods separated by comma (,)] \
         ; [URI]"
        wtr
    else if len = 2 then (List.nth tokens 0, List.nth tokens 1)
      (* Default method is `GET *)
    else ("get", List.nth tokens 0)
  in
  match request_target_tokens uri with
  | Ok (path_tokens, query_tokens) ->
      let methods' = make_methods ~loc methods in
      let uri = make_request_target ~loc query_tokens path_tokens in
      [%expr Wtr.routes [%e methods'] [%e uri]]
  | Error msg -> Location.raise_errorf ~loc "wtr: %s" msg

let routes_ppx_name = "routes"

let routes_ppx =
  Extension.declare routes_ppx_name Extension.Context.Expression
    Ast_pattern.(single_expr_payload (estring __))
    make_routes

let () =
  Driver.register_transformation routes_ppx_name ~extensions:[ routes_ppx ]
OCaml

Innovation. Community. Security.