├── .gitignore
├── .ocamlformat
├── README.md
├── bin
├── dune
├── main.ml
└── migrate.ml
├── dune-project
├── lib
├── components
│ ├── ExhibitDetailed.ml
│ ├── ExhibitList.ml
│ ├── ExhibitPost.ml
│ ├── ImageUpload.ml
│ └── dune
├── database.ml
├── database.sql
├── dune
├── hx
│ ├── css.ml
│ ├── dune
│ └── hx.ml
├── models
│ ├── dune
│ ├── exhibit.ml
│ ├── image.ml
│ └── user.ml
├── resource.ml
└── sql_setup.eml.ml
├── ohtml.opam
├── ohtml.opam.locked
├── ppx-model
├── dune
├── model.ml
└── pp.ml
├── ppx_model.opam
├── principles.md
├── scratch
├── aria.md
├── example.ml
└── rsjs.md
├── static
└── home.css
├── tables
├── 001-dream_session.sql
├── 010-image.sql
├── 010-user.sql
├── 020-exhibits.sql
└── 030-comments.sql
└── test
└── ppx
├── dune
├── pp.ml
├── test.expected.ml
└── test.ml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.annot
2 | *.cmo
3 | *.cma
4 | *.cmi
5 | *.a
6 | *.o
7 | *.cmx
8 | *.cmxs
9 | *.cmxa
10 |
11 | # ocamlbuild working directory
12 | _build/
13 |
14 | # ocamlbuild targets
15 | *.byte
16 | *.native
17 |
18 | # oasis generated files
19 | setup.data
20 | setup.log
21 |
22 | # Merlin configuring file for Vim and Emacs
23 | .merlin
24 |
25 | # Dune generated files
26 | *.install
27 |
28 | # Local OPAM switch
29 | _opam/
30 |
31 | # duniverse is from opam-monorepo
32 | duniverse/
33 |
34 | *.scip
35 |
36 | db.sqlite
37 | database/
38 |
39 | node_modules/
40 | _esy/
41 | esy.lock/
42 |
43 | profile.dump
44 |
--------------------------------------------------------------------------------
/.ocamlformat:
--------------------------------------------------------------------------------
1 | version = 0.25.1
2 | profile = janestreet
3 |
4 | margin = 90
5 | doc-comments-padding = 100
6 | exp-grouping = preserve
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ohtml
2 |
3 | Ohtml (oatmeal) is a project focused on making the web fun, safe, and fast.
4 |
5 | ## Goals
6 |
7 | Roughly speaking (copied from a recent tweet of mine):
8 |
9 | I'm exploring building a framework around htmx inside of ocaml, with some goals roughly approximating:
10 | - Generate (via ppx) all the CRUD operations on your data types
11 | - Generate all the CRUD routes on your data types
12 | - Typesafe htmx usage within TyXML (so you can generate your forms with complete typesafety from DB to generated HTML/Routes).
13 | - Shouldn't have to write any javascript (but phase two will hopefully include Melange compilation, so you can write your JS in ocaml)
14 | - Builtin stuff with Dream, cause it's a cool webserver (so, easy integration with Dream's handlers, forms, sessions, etc)
15 |
16 | In some ways, you could imagine it as typesafe, fast rails with typechecked templates (but the templates are actually just functions, since it's ocaml)
17 |
18 | ## Installation (WIP)
19 |
20 | You'll need to have:
21 |
22 | ```bash
23 | # First, you'll need opam. Install that based on ocaml.org recommendations
24 |
25 | $ git clone https://github.com/tjdevries/ohtml
26 | $ cd ohtml
27 |
28 | # Create a new opam environment to run for this project
29 | $ opam switch create . 5.0.0
30 |
31 | # Install opam monorepo, which we use to build the project
32 | $ opam install opam-monorepo
33 |
34 | # (Temporary)
35 | $ opam pin add dune.3.8 --dev-repo
36 |
37 | # Pull the deps and install
38 | $ opam monorepo pull
39 |
40 | # Run it!
41 | $ dune exec ohtml
42 | ```
43 |
--------------------------------------------------------------------------------
/bin/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (public_name ohtml)
3 | (name main)
4 | (package ohtml)
5 | (modules main)
6 | ; This should make TyXML error messages easier to read? Will try it out later.
7 | (flags :standard -short-paths)
8 | (libraries ohtml hx components dream dream-livereload tyxml models
9 | caqti caqti-lwt caqti-dynload caqti-driver-sqlite3)
10 | (preprocess (pps ppx_let lwt_ppx)))
11 |
12 | (executable
13 | (public_name migrate)
14 | (name migrate)
15 | (package ohtml)
16 | (modules migrate)
17 | (libraries
18 | ohtml
19 | dream tyxml caqti caqti-lwt caqti-dynload caqti-driver-sqlite3)
20 | (preprocess (pps lwt_ppx)))
21 |
--------------------------------------------------------------------------------
/bin/main.ml:
--------------------------------------------------------------------------------
1 | open Base
2 | module Exhibit = Models.Exhibit
3 |
4 | let elt_to_string elt = Fmt.str "%a" (Tyxml.Html.pp_elt ()) elt
5 |
6 | let unwrap x =
7 | match x with
8 | | Ok x -> x
9 | | _ -> failwith "unwrap failed!"
10 | ;;
11 |
12 | let make_image link_ =
13 | let open Tyxml.Html in
14 | img ~src:link_ ~alt:"" ()
15 | ;;
16 |
17 | let make_swapper route content t =
18 | let open Tyxml.Html in
19 | div
20 | ~a:[ a_class [ "sample-transition" ] ]
21 | [ h1 [ txt "hey begin: "; txt content ]
22 | ; button
23 | ~a:[ Hx.get route; Hx.swap ~transition:true OuterHTML; Hx.target (Closest "div") ]
24 | [ txt t ]
25 | ]
26 | ;;
27 |
28 | let default_header title_text =
29 | let open Tyxml.Html in
30 | head
31 | (title (txt title_text))
32 | [ link ~rel:[ `Stylesheet ] ~href:"/static/home.css" ()
33 | (* ; script ~a:[ a_mime_type "module"; a_src "https://cdn.skypack.dev/twind/shim" ] (txt "") *)
34 | ; script ~a:[ a_src "https://unpkg.com/htmx.org@1.9.0" ] (txt "")
35 | ]
36 | ;;
37 |
38 | let index _ who =
39 | let open Tyxml.Html in
40 | html
41 | (default_header "OhtML")
42 | (body
43 | [ h1 [ txt "Good morning, "; txt who; txt "!" ]
44 | ; div ~a:[ a_id "counter"; a_class [ "bg-gray-200" ] ] [ txt "0" ]
45 | ; h2 [ txt "not affiliated with rust foundation, btw" ]
46 | ; button ~a:[ Hx.post "/increment" ] [ txt "Increment" ]
47 | ; make_swapper "/transition" "swapped content" "swap it!"
48 | ; a ~a:[ a_href "/exhibits" ] [ txt "exhibits" ]
49 | ; img ~src:"" ~alt:"" ()
50 | ; div
51 | @@ Components.ImageUpload.make_form
52 | ~id:"image"
53 | ~route:"/upload"
54 | ~target:(Previous "img")
55 | ])
56 | ;;
57 |
58 | let login request =
59 | let redirect = Dream.query request "redirect" in
60 | let open Tyxml.Html in
61 | let make_input ~name ~text ~input_type =
62 | div
63 | [ label ~a:[ a_label_for name ] [ txt text ]
64 | ; input ~a:[ a_id name; a_name name; a_input_type input_type ] ()
65 | ]
66 | in
67 | html
68 | (default_header "Login")
69 | (body
70 | [ h1 [ txt "Login here" ]
71 | ; form
72 | ~a:
73 | [ (a_action
74 | @@ "/login"
75 | ^
76 | match redirect with
77 | | Some redirect -> "?redirect=/" ^ redirect
78 | | None -> "")
79 | ; a_method `Post (* ; a_content "application/x-www-form-urlencoded" *)
80 | ]
81 | (* [ input *)
82 | (* ~a: *)
83 | (* [ a_name "dream.csrf" *)
84 | (* ; a_input_type `Hidden *)
85 | (* ; a_value (Dream.csrf_token request) *)
86 | (* ] *)
87 | (* () *)
88 | [ Dream.csrf_tag request |> Unsafe.data
89 | ; div
90 | ~a:[ a_id "twitchchat" ]
91 | [ make_input ~name:"name" ~text:"Username:" ~input_type:`Text
92 | ; make_input ~name:"password" ~text:"Password:" ~input_type:`Password
93 | ; button ~a:[ a_button_type `Submit ] [ txt "Submit" ]
94 | ]
95 | ]
96 | ])
97 | ;;
98 |
99 | let html_to_string html = Fmt.str "%a" (Tyxml.Html.pp ()) html
100 |
101 | let format_exhibits user_id request =
102 | let%lwt user =
103 | match user_id with
104 | | Some user_id -> Dream.sql request (Models.User.read ~id:user_id)
105 | | None -> None |> Lwt_result.return
106 | in
107 | match user with
108 | | Ok user ->
109 | (match%lwt Dream.sql request (Exhibit.read_all ()) with
110 | | Ok exhibits ->
111 | Components.ExhibitList.page user (default_header "Exhibits") exhibits
112 | | _ -> failwith "No Exhibists Something")
113 | | Error _ -> failwith "Error Something"
114 | ;;
115 |
116 | let counter = ref 0
117 |
118 | let get_user request =
119 | let user_id = Dream.session_field request "user" in
120 | match user_id with
121 | | None ->
122 | let%lwt () = Dream.invalidate_session request in
123 | let%lwt () = Dream.set_session_field request "user" "1" in
124 | None |> Lwt.return
125 | | Some user_id -> Some (Int.of_string user_id) |> Lwt.return
126 | ;;
127 |
128 | let set_user ?(redirect = "/") request user_id =
129 | let%lwt () = Dream.invalidate_session request in
130 | let%lwt () = Dream.set_session_field request "user" (Int.to_string user_id) in
131 | Dream.redirect request redirect
132 | ;;
133 |
134 | let () =
135 | Dream.run
136 | @@ Dream.logger
137 | @@ Dream_livereload.inject_script ()
138 | @@ Dream.sql_pool "sqlite3:/home/tjdevries/tmp/ohtml.sqlite"
139 | @@ Dream.memory_sessions
140 | @@ Dream.router
141 | [ Dream.get "/" (fun request ->
142 | Dream.html
143 | @@ html_to_string
144 | @@ index request (Fmt.str "Twitch Chat: %s" "teej"))
145 | ; Dream.get "/static/**" (Dream.static "./static")
146 | ; Dream.get "/login" (fun request -> Dream.html @@ html_to_string (login request))
147 | ; Dream.post "/login" (fun request ->
148 | let find_data t key =
149 | List.find_map t ~f:(fun (header, data) ->
150 | if String.(header = key) then Some data else None)
151 | in
152 | let redirect =
153 | Dream.query request "redirect" |> Base.Option.value ~default:"/"
154 | in
155 | let%lwt form_result = Dream.form request in
156 | match form_result with
157 | | `Ok form_data ->
158 | let username = find_data form_data "name" in
159 | let password = find_data form_data "password" in
160 | (match username, password with
161 | | Some username, Some password ->
162 | let%lwt user = Models.User.login_user request username password in
163 | (match user with
164 | | Ok user -> set_user ~redirect request user.id
165 | | _ -> failwith "bad user or somethin")
166 | | _ -> failwith "dawg")
167 | | `Expired _ -> failwith "EXPIRED"
168 | | `Wrong_session _ -> failwith "wrong sessions"
169 | | `Invalid_token _ -> failwith "invalid token"
170 | | `Missing_token _ -> failwith "mising token"
171 | | `Many_tokens _ -> failwith "many tokens"
172 | | `Wrong_content_type -> failwith "wrong content Something")
173 | ; Dream.post "/increment" (fun _ ->
174 | Int.incr counter;
175 | Dream.html ("yo, posted:" ^ Int.to_string !counter))
176 | ; Dream.get "/count" (fun _ ->
177 | Dream.html ("yo, count is:" ^ Int.to_string !counter))
178 | ; Dream.get "/exhibits" (fun request ->
179 | let%lwt user_id = get_user request in
180 | format_exhibits user_id request)
181 | ; Dream.post "/exhibit/" (fun request ->
182 | (* TODO: Come back to this and explore let%map and let%bind more *)
183 | (* let open Result.Let_syntax in *)
184 | (* let open Lwt_result.Let_syntax in *)
185 | let%lwt user_id = get_user request in
186 | match user_id with
187 | | Some user_id ->
188 | let%lwt form_result = Dream.multipart ~csrf:false request in
189 | (match form_result with
190 | | `Ok data ->
191 | let%lwt decoded = Components.ExhibitPost.upload_form request data in
192 | let content, image =
193 | match decoded with
194 | | Ok (content, image) -> content, image
195 | | Error _ -> assert false
196 | in
197 | let image_id = Option.map image ~f:(fun x -> x.id) in
198 | (match%lwt
199 | Dream.sql request (Models.Exhibit.create ~user_id ~content ~image_id)
200 | with
201 | | Ok exhibit ->
202 | Dream.response
203 | (elt_to_string @@ Components.ExhibitList.table_row None exhibit)
204 | |> Lwt.return
205 | | _ -> assert false)
206 | | _ -> Dream.empty `Bad_Request)
207 | | None -> Dream.empty `Unauthorized)
208 | ; Dream.get "/exhibit/list/:id" (fun request ->
209 | let id = Dream.param request "id" in
210 | let%lwt exhibit = Dream.sql request (Exhibit.read ~id:(Int.of_string id)) in
211 | match exhibit with
212 | | Ok exhibit ->
213 | Dream.response
214 | (elt_to_string
215 | @@ Components.ExhibitList.table_row None (Option.value_exn exhibit))
216 | |> Lwt.return
217 | | _ -> Dream.empty `Not_Found)
218 | ; Dream.get "/exhibit/:id"
219 | @@ Components.ExhibitDetailed.handle (default_header "exhibits")
220 | ; Dream.delete "/exhibit/:id" (fun request ->
221 | let id = Dream.param request "id" in
222 | let%lwt exhibit = Dream.sql request (Exhibit.delete ~id:(Int.of_string id)) in
223 | match exhibit with
224 | | Ok _ -> Dream.html "deleted"
225 | | _ -> Dream.empty `Not_Found)
226 | ; Dream.get "/user/:id" Models.User.handle_get
227 | ; Dream.get "/replace" (fun _ ->
228 | Dream.html
229 | (make_swapper "/transition" "swapped content" "swap it!"
230 | |> Fmt.str "%a" (Tyxml.Html.pp_elt ())))
231 | ; Dream.get "/transition" (fun _ ->
232 | Dream.html
233 | (make_swapper "/replace" "replace this" "replace it!"
234 | |> Fmt.str "%a" (Tyxml.Html.pp_elt ())))
235 | ; Dream.post "/upload" (fun request ->
236 | let rec receive files =
237 | match%lwt Dream.upload request with
238 | | None -> files |> Lwt.return
239 | | Some (_, _, _) ->
240 | let rec concat_part contents =
241 | match%lwt Dream.upload_part request with
242 | | None -> receive (contents :: files)
243 | | Some chunk -> concat_part (contents ^ chunk)
244 | in
245 | concat_part ""
246 | in
247 | let%lwt uploaded = receive [] in
248 | let%lwt image =
249 | Dream.sql request Models.Image.(create ~data:(List.hd_exn uploaded))
250 | in
251 | let image = image |> unwrap in
252 | Dream.html (elt_to_string (make_image ("/images/" ^ Int.to_string image.id))))
253 | ; Dream.get "/images/:id" (fun request ->
254 | let id = Dream.param request "id" in
255 | let id = Int.of_string id in
256 | let%lwt image = Dream.sql request (Models.Image.read ~id) in
257 | let image = image |> unwrap in
258 | let image = Option.value_exn image in
259 | Dream.response ~headers:[ "Content-Type", "image/jpeg" ] image.data
260 | |> Lwt.return)
261 | ; Dream_livereload.route ()
262 | (* ; Dream.get "/exhibit/" (fun _ -> Dream.html "POSTED YO") *)
263 | ]
264 | ;;
265 |
--------------------------------------------------------------------------------
/bin/migrate.ml:
--------------------------------------------------------------------------------
1 | let () = print_endline "Migrate"
2 |
3 | let unwrap m =
4 | match%lwt m with
5 | | Ok a -> a |> Lwt.return
6 | | Error e -> failwith @@ Caqti_error.show e
7 | ;;
8 |
9 | let main () =
10 | (* Kill existing db *)
11 | (try Unix.unlink "/home/tjdevries/tmp/ohtml.sqlite" with
12 | | _ -> ());
13 | (* Migrate new db *)
14 | let connection = Uri.of_string "sqlite3:/home/tjdevries/tmp/ohtml.sqlite" in
15 | let%lwt connection = Caqti_lwt.connect connection |> unwrap in
16 | let%lwt _ = Ohtml.Sql_setup.migrate connection in
17 | Ohtml.Sql_setup.populate connection
18 | ;;
19 |
20 | (* Lwt_main.run connection *)
21 |
22 | let _ = Lwt_main.run @@ main ()
23 |
--------------------------------------------------------------------------------
/dune-project:
--------------------------------------------------------------------------------
1 | (lang dune 3.7)
2 | (cram enable)
3 |
4 | (name ohtml)
5 |
6 | (generate_opam_files true)
7 | (source (github tjdevries/OhtML))
8 | (authors "TJ DeVries")
9 | (maintainers "TJ DeVries")
10 | (license MIT)
11 | ; (documentation https://url/to/documentation)
12 |
13 | (package
14 | (name ohtml)
15 | (synopsis "OhtML (Oatmeal) is a web framework where you get to have fun.")
16 | (description "No, seriously. The goal really is to have fun")
17 | (depends (ocaml (>= "5.0"))
18 | dune
19 | dream dream-livereload
20 | tyxml
21 | bos
22 | base ppx_let fmt
23 | lwt caqti caqti-lwt caqti-dynload caqti-driver-sqlite3
24 | ppx_rapper ppx_rapper_lwt))
25 |
26 | (package
27 | (name ppx_model)
28 | (depends ocaml ppxlib))
29 |
--------------------------------------------------------------------------------
/lib/components/ExhibitDetailed.ml:
--------------------------------------------------------------------------------
1 | module User = Models.User
2 | module Exhibit = Models.Exhibit
3 |
4 | let unwrap x =
5 | match x with
6 | | Ok x -> x
7 | | _ -> failwith "nope"
8 | ;;
9 |
10 | let format_exhibit header (user : User.t) (exhibit : Exhibit.t) (cont : string list) =
11 | let e_header = header in
12 | let open Tyxml.Html in
13 | let comments = List.map ~f:(fun c -> tr [ td [ txt c ] ]) cont in
14 | let image_tag =
15 | match exhibit.image_id with
16 | | Some image_id -> img ~src:("/images/" ^ Int.to_string image_id) ~alt:"" ()
17 | | None -> div []
18 | in
19 | let e_body =
20 | body
21 | [ div
22 | ~a:[ a_id ("exhibit-" ^ Int.to_string exhibit.id) ]
23 | [ txt exhibit.content
24 | ; txt " ("
25 | ; txt user.name
26 | ; txt ")"
27 | ; image_tag
28 | ; table ~thead:(thead [ tr [ th [ txt "comments" ] ] ]) comments
29 | ]
30 | ]
31 | in
32 | html e_header e_body |> Fmt.str "%a" (Tyxml.Html.pp_elt ())
33 | ;;
34 |
35 | let handle header (request : Dream.request) =
36 | let id = Dream.param request "id" in
37 | let%lwt exhibit = Dream.sql request (Exhibit.read ~id:(Int.of_string id)) in
38 | match exhibit with
39 | | Ok exhibit ->
40 | let exhibit = Option.value_exn exhibit in
41 | let%lwt user = Dream.sql request (User.read ~id:exhibit.user_id) in
42 | let user =
43 | match user with
44 | | Ok user -> user
45 | | Error e ->
46 | Fmt.pr "@.%a@." Caqti_error.pp e;
47 | failwith "OH NO"
48 | in
49 | let user = user |> Option.value_exn in
50 | let%lwt comments = Dream.sql request (Exhibit.get_comments ~id:exhibit.id) in
51 | (match comments with
52 | | Ok comments -> Dream.html @@ format_exhibit header user exhibit comments
53 | | Error _ -> Dream.empty `Not_Found)
54 | | _ -> Dream.empty `Not_Found
55 | ;;
56 |
--------------------------------------------------------------------------------
/lib/components/ExhibitList.ml:
--------------------------------------------------------------------------------
1 | open Tyxml.Html
2 | module Exhibit = Models.Exhibit
3 |
4 | (* TODO: Not sure I like this very much *)
5 | type t = Exhibit.t
6 |
7 | let table_head = thead [ tr [ th [ txt "id" ]; th [ txt "user" ]; th [ txt "content" ] ] ]
8 |
9 | let table_row (user : Models.User.t option) (ex : t) =
10 | tr
11 | [ td [ txt @@ Int.to_string ex.id ]
12 | ; td [ txt (ex.user_id |> Int.to_string) ]
13 | ; td [ txt ex.content ]
14 | ; td [ a ~a:[ a_href (Exhibit.route_for ex) ] [ txt "get" ] ]
15 | ; (match user with
16 | | Some user when user.id = ex.user_id ->
17 | td
18 | [ button
19 | ~a:
20 | [ Hx.delete (Exhibit.route_for ex)
21 | ; Hx.swap OuterHTML
22 | ; Hx.target (Closest "button")
23 | ]
24 | [ txt "delete" ]
25 | ]
26 | | _ -> td [ txt "" ])
27 | ]
28 | ;;
29 |
30 | let page user page_header exhibits =
31 | let open Tyxml.Html in
32 | let exhibits = List.map exhibits ~f:(table_row user) in
33 | let login =
34 | match user with
35 | | None -> [ div [ a ~a:[ a_href "/login?redirect=exhibits" ] [ txt "login" ] ] ]
36 | | _ -> []
37 | in
38 | let exhibits =
39 | div [ table ~thead:table_head exhibits ]
40 | ::
41 | (match user with
42 | | Some _ -> [ ExhibitPost.form ~target:(Previous "tbody") ]
43 | | None -> [])
44 | in
45 | html page_header (body (login @ exhibits))
46 | |> Fmt.str "%a" (Tyxml.Html.pp ())
47 | |> Dream.html
48 | ;;
49 |
--------------------------------------------------------------------------------
/lib/components/ExhibitPost.ml:
--------------------------------------------------------------------------------
1 | (* open Models *)
2 |
3 | let _content_id = "post-content"
4 | let _image_id = "post-image"
5 |
6 | let form ~target =
7 | let open Tyxml.Html in
8 | div
9 | ~a:[ a_id "exhibit-form" ]
10 | [ form
11 | ~a:
12 | [ Hx.post "/exhibit/"
13 | ; Hx.target target
14 | ; Hx.swap BeforeEnd (* ; Hx.boost true *)
15 | ; a_enctype "multipart/form-data"
16 | ]
17 | [ div [ input ~a:[ a_name _content_id ] () ]
18 | ; div
19 | [ label ~a:[ a_label_for _image_id ] [ txt "image goes here: " ]
20 | ; input ~a:[ a_input_type `File; a_name _image_id ] ()
21 | ]
22 | ; input ~a:[ a_input_type `Submit; a_value "Create Exhibit" ] ()
23 | ]
24 | ]
25 | ;;
26 |
27 | module FormHelper = struct
28 | type 'a t = (string * 'a) list
29 |
30 | let find_data t key =
31 | List.find_map t ~f:(fun (header, data) ->
32 | if String.(header = key) then Some data else None)
33 | ;;
34 | end
35 |
36 | let upload_form request (form_data : Dream.multipart_form) =
37 | let content =
38 | match FormHelper.find_data form_data _content_id with
39 | | Some [ (_, content) ] -> Some content
40 | | _ -> None
41 | in
42 | let%lwt image_data =
43 | match FormHelper.find_data form_data _image_id with
44 | | Some image_data when List.length image_data = 1 ->
45 | (* TODO: Not sure if we should discard the file name here... but i don't really care *)
46 | let _, image_data = List.hd_exn image_data in
47 | let%lwt image = Dream.sql request Models.Image.(create ~data:image_data) in
48 | (match image with
49 | | Ok image -> Some image |> Lwt.return
50 | | _ -> None |> Lwt.return)
51 | | _ -> None |> Lwt.return
52 | in
53 | match content, image_data with
54 | | Some content, image_data -> Ok (content, image_data) |> Lwt.return
55 | | _ -> Error `MissingContent |> Lwt.return
56 | ;;
57 |
--------------------------------------------------------------------------------
/lib/components/ImageUpload.ml:
--------------------------------------------------------------------------------
1 | let make_form ~id ~route ~target =
2 | let open Tyxml.Html in
3 | [ form
4 | ~a:
5 | [ a_id id
6 | ; Hx.post route
7 | ; Hx.swap OuterHTML
8 | ; Hx.target target
9 | ; a_enctype "multipart/form-data"
10 | ]
11 | [ label ~a:[ a_label_for id ] [ txt "image goes here: " ]
12 | ; input ~a:[ a_input_type `File; a_name "image" ] ()
13 | ; input ~a:[ a_input_type `Submit; a_value "Upload" ] ()
14 | ]
15 | ]
16 | ;;
17 |
--------------------------------------------------------------------------------
/lib/components/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name components)
3 | (libraries base tyxml ohtml hx models)
4 | (flags (:standard -w -11 -open Base))
5 | (preprocess (pps lwt_ppx)))
6 |
7 |
--------------------------------------------------------------------------------
/lib/database.ml:
--------------------------------------------------------------------------------
1 | module type DB = Caqti_lwt.CONNECTION
2 |
3 | type t = (module DB)
4 | type error = Database_error of string
5 |
6 | let or_error m =
7 | match%lwt m with
8 | | Ok a -> Ok a |> Lwt.return
9 | | Error e -> Error (Database_error (Caqti_error.show e)) |> Lwt.return
10 | ;;
11 |
--------------------------------------------------------------------------------
/lib/database.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE exhibits (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | content TEXT NOT NULL
4 | )
5 |
--------------------------------------------------------------------------------
/lib/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name ohtml)
3 | (modules sql_setup database resource)
4 | (libraries lwt caqti-lwt bos base ppx_rapper_lwt tyxml dream)
5 | (preprocess (pps ppx_rapper lwt_ppx))
6 | (preprocessor_deps (glob_files *.sql)))
7 |
8 | (rule
9 | (targets sql_setup.ml)
10 | (deps sql_setup.eml.ml)
11 | (action (run dream_eml %{deps} --workspace %{workspace_root})))
12 |
--------------------------------------------------------------------------------
/lib/hx/css.ml:
--------------------------------------------------------------------------------
1 | (** Get the name of some HTML element.
-> div *)
2 | let get_name elt =
3 | let elt = Tyxml.Html.toelt elt in
4 | let doc = Tyxml.Xml.content elt in
5 | match doc with
6 | | Tyxml_xml.Leaf (name, _) -> name
7 | | Tyxml_xml.Node (name, _, _) -> name
8 | | _ -> failwith "I don't think this is possible from html..."
9 | ;;
10 |
11 | module Selector = struct
12 | type selector =
13 | | Raw of string
14 | | Type of string
15 | | Class of string
16 | | Id of string
17 | | Attribute of string
18 |
19 | type t =
20 | | This
21 | | Raw of string
22 | | Closest of selector
23 | end
24 |
25 | (* TODO: Can use sig thing when I'm done?... *)
26 | module Namespace = struct
27 | type t = string
28 |
29 | let create (x : string) : t = x
30 | let none = create ""
31 | let universal = create "*"
32 | end
33 |
34 | module Element = struct
35 | type t = string
36 |
37 | let element elt =
38 | let _ = get_name elt in
39 | (* { attribute = name; value = None } *)
40 | assert false
41 | ;;
42 | end
43 |
44 | module AttributeSelector = struct
45 | type value =
46 | { operation : [ `Equal | `OneOf | `Exactly | `Prefix | `Suffix | `Contains ]
47 | ; namespace : Namespace.t option
48 | ; value : [ `Identifier of string | `String of string ]
49 | }
50 |
51 | type t =
52 | { attribute : string
53 | ; value : value option
54 | }
55 | end
56 |
57 | module Simple = struct
58 | type t =
59 | | Type of Namespace.t option * Element.t
60 | | Universal
61 | | Attribute of AttributeSelector.t
62 |
63 | let s_type elt = Type (None, elt)
64 | let s_universal = Universal
65 | end
66 |
--------------------------------------------------------------------------------
/lib/hx/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name hx)
3 | (libraries base fmt dream tyxml)
4 | (flags (:standard -w -11 -open Base)))
5 |
6 |
--------------------------------------------------------------------------------
/lib/hx/hx.ml:
--------------------------------------------------------------------------------
1 | (* TODO: hx-trigger response
2 | You can send a response header with basically an event name
3 | and then respond to that from within the rest of the body.
4 | So, you could auto refresh / re-update some table with a new HTTP
5 | request, but decoupled from the rest of the project.
6 |
7 | Smart thing to do would be to make some type/module that has events
8 | and use that from within your own stuff to make sure you're only emiting
9 | events that can be handled. Something to think about.
10 |
11 | https://hypermedia.systems/book/deep-htmx/ -> Server Generated Events *)
12 |
13 | (*
14 | HX-Location
15 | Causes a client-side redirection to a new location
16 |
17 | HX-Push-Url
18 | Pushes a new URL into the location bar
19 |
20 | HX-Refresh
21 | Refreshes the current page
22 |
23 | HX-Retarget
24 | Allows you to specify a new target to swap the response content into on the client side *)
25 |
26 | (* *)
27 |
28 | open Base
29 |
30 | (** Typesafe htmlx wrappers for OCaml. Emits attributes to be used with TyXML *)
31 |
32 | (* TODO: Would be cool to break this down a bit more (class, id, etc.).
33 | Down with strings! *)
34 | type css_selector = string
35 |
36 | (** hx-get wrapper. TODO: Should use different link type *)
37 | let get link = Tyxml.Html.Unsafe.string_attrib "hx-get" link
38 |
39 | (** hx-post wrapper. TODO: Shoudl use different link type *)
40 | let post link = Tyxml.Html.Unsafe.string_attrib "hx-post" link
41 |
42 | (** hx-delete wrapper. TODO: Should use different link type *)
43 | let delete str = Tyxml.Html.Unsafe.string_attrib "hx-delete" str
44 |
45 | module TargetType = struct
46 | type t =
47 | | This
48 | | Css of css_selector
49 | | Closest of css_selector
50 | | Find of css_selector
51 | | Previous of css_selector
52 |
53 | let to_string = function
54 | | This -> "this"
55 | | Css s -> s
56 | | Closest s -> "closest " ^ s
57 | | Find s -> "find " ^ s
58 | | Previous s -> "previous " ^ s
59 | ;;
60 | end
61 |
62 | (** Typesafe hx-target wrapper *)
63 | let target t = Tyxml.Html.Unsafe.string_attrib "hx-target" (TargetType.to_string t)
64 |
65 | (** hx-select. Select the content to swap into the page *)
66 | let select sel = Tyxml.Html.Unsafe.string_attrib "hx-select" sel
67 |
68 | let select_oob sel =
69 | Tyxml.Html.Unsafe.string_attrib "hx-select" (String.concat ~sep:"," sel)
70 | ;;
71 |
72 | module SwapType = struct
73 | module Modifiers = struct
74 | type scrolldir =
75 | | Top
76 | | Bottom
77 |
78 | type scrolling = scrolldir * string option
79 |
80 | let scroll_to_string prefix scroll =
81 | let map_dir = function
82 | | Top -> "top"
83 | | Bottom -> "bottom"
84 | in
85 | match scroll with
86 | | dir, Some modifier -> Fmt.str "%s:%s:%s" prefix modifier (map_dir dir)
87 | | dir, None -> Fmt.str "%s:%s" prefix (map_dir dir)
88 | ;;
89 |
90 | type data =
91 | { transition : bool option
92 | ; swap : string option
93 | ; settle : string option
94 | ; scroll : scrolling option
95 | ; show : scrolling option
96 | ; focus_scroll : bool option
97 | }
98 |
99 | type t = data option
100 |
101 | let create ~transition ~swap ~settle ~scroll ~show ~focus_scroll =
102 | match transition, swap with
103 | | None, None -> None
104 | | _, _ -> Some { transition; swap; settle; scroll; show; focus_scroll }
105 | ;;
106 |
107 | let to_string = function
108 | | None -> ""
109 | | Some data ->
110 | [ Some "" (* Empty string to start, to prefix with space *)
111 | ; Option.map data.transition ~f:(Fmt.str "transition:%b")
112 | ; Option.map data.swap ~f:(Fmt.str "swap:%s")
113 | ; Option.map data.settle ~f:(Fmt.str "settle:%s")
114 | ; Option.map data.scroll ~f:(scroll_to_string "scroll")
115 | ; Option.map data.show ~f:(scroll_to_string "show")
116 | ; Option.map data.focus_scroll ~f:(Fmt.str "focus-scroll:%b")
117 | ]
118 | |> List.filter_opt
119 | |> String.concat ~sep:" "
120 | ;;
121 | (* Option.map *)
122 | end
123 |
124 | type attr =
125 | | InnerHTML
126 | (** The default, replace the inner html of the target element *)
127 | | OuterHTML
128 | (** Replace the entire target element with the response *)
129 | | BeforeBegin
130 | (** Insert the response before the target element *)
131 | | AfterBegin
132 | (** Insert the response before the first child of the target element *)
133 | | BeforeEnd
134 | (** Insert the response after the last child of the target element *)
135 | | AfterEnd
136 | (** Insert the response after the target element *)
137 | | Delete
138 | (** Deletes the target element regardless of the response *)
139 | | None
140 | (** Does not append content from response (out of band items will still be processed). *)
141 |
142 | type t =
143 | { attr : attr
144 | ; modifiers : Modifiers.t
145 | }
146 |
147 | let to_attr t =
148 | let attr =
149 | match t.attr with
150 | | InnerHTML -> "innerHTML"
151 | | OuterHTML -> "outerHTML"
152 | | BeforeBegin -> "beforebegin"
153 | | AfterBegin -> "afterbegin"
154 | | BeforeEnd -> "beforeend"
155 | | AfterEnd -> "afterend"
156 | | Delete -> "delete"
157 | | None -> "none"
158 | in
159 | let modifiers = Modifiers.to_string t.modifiers in
160 | Tyxml.Html.Unsafe.string_attrib "hx-swap" (attr ^ modifiers)
161 | ;;
162 | end
163 |
164 | (** Typesafe hx-swap wrapper *)
165 | let swap ?transition ?swap ?settle ?scroll ?show ?focus_scroll attr =
166 | let open SwapType in
167 | to_attr
168 | { attr
169 | ; modifiers = Modifiers.create ~transition ~swap ~settle ~scroll ~show ~focus_scroll
170 | }
171 | ;;
172 |
173 | module TriggerType = struct
174 | type trigger =
175 | | Event of string
176 | | Every of string
177 |
178 | type modifier =
179 | | Once
180 | | Changed
181 | | Delay of string
182 | | Throttle of string
183 | | Target of string
184 | | From of [ `Document | `Window | `Closest of string | `Find of string ]
185 | | Consume
186 | | Queue of [ `First | `Last | `All | `None ]
187 | | Load
188 | (** Trigged on load (useful for lazy-loading) *)
189 | | Revealed
190 | (** Triggered when an element is scrolled into the viewport (useful for lazy-loading).
191 | If you are using `overflow` you should `intersect once` instead of Revealed *)
192 | | Intersect of [ `Default | `Root of string | `Threshold of float ]
193 |
194 | type t = trigger list * modifier list
195 |
196 | let init ?(modifiers = []) triggers = triggers, modifiers
197 | end
198 |
199 | (* TODO: hx-swap-oob? I don't think I understand. Need to look at some more examples. *)
200 |
201 | (* Misc Attributes *)
202 | let boost status = Tyxml.Html.Unsafe.string_attrib "hx-boost" (Bool.to_string status)
203 |
204 | let push_url status =
205 | Tyxml.Html.Unsafe.string_attrib "hx-push-url" (Bool.to_string status)
206 | ;;
207 |
208 | let indicator elt =
209 | let _ = elt in
210 | assert false
211 | ;;
212 |
213 | (* TODO: hx-on. Would be very cool to use melange and encode the function name into some script:
214 | https://htmx.org/attributes/hx-on/ *)
215 |
216 | module Headers = struct
217 | (* don't expose *)
218 | let htmx_truthy_header header req =
219 | Dream.headers req header
220 | |> List.find ~f:(fun header -> String.(header = "true"))
221 | |> Option.is_some
222 | ;;
223 |
224 | let is_htmx req = htmx_truthy_header "HX-Request" req
225 | let is_boosted req = htmx_truthy_header "HX-Boosted" req
226 | let is_history_restore req = htmx_truthy_header "HX-History-Restore-Request" req
227 |
228 | (** This will contain the user response to an hx-prompt *)
229 | let get_prompt req = Dream.headers req "HX-Prompt" |> List.hd
230 |
231 | (** This value will be the id of the target element, if it exists *)
232 | let get_target_id req = Dream.headers req "HX-Target" |> List.hd
233 |
234 | (** This value will be the id of the triggered element, if it exists *)
235 | let get_trigger_id req = Dream.headers req "HX-Trigger" |> List.hd
236 |
237 | (** This value will be the name of the triggered element, if it exists *)
238 | let get_trigger_name req = Dream.headers req "HX-Trigger-Name" |> List.hd
239 | end
240 |
--------------------------------------------------------------------------------
/lib/models/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name models)
3 | (modules user image exhibit)
4 | (libraries dream tyxml lwt caqti-lwt ppx_rapper_lwt)
5 | (preprocess (pps ppx_model ppx_rapper lwt_ppx)))
6 |
--------------------------------------------------------------------------------
/lib/models/exhibit.ml:
--------------------------------------------------------------------------------
1 | module ExhibitID = struct
2 | type t = int
3 |
4 | let t =
5 | let encode (t : t) : (int, string) result = Ok t in
6 | let decode (t : int) : (t, string) result = Ok t in
7 | Caqti_type.(custom ~encode ~decode int)
8 | ;;
9 |
10 | let mk (t : int) : t = t
11 | end
12 |
13 | module _ : Rapper.CUSTOM = ExhibitID
14 | module UserID = User.UserID
15 |
16 | type t =
17 | { id : ExhibitID.t
18 | ; user_id : int
19 | ; image_id : int option
20 | ; content : string
21 | }
22 | [@@deriving model ~table_name:"exhibits"]
23 |
24 | let delete = [%rapper execute "DELETE FROM exhibits WHERE id = %int{id}"]
25 | let route = "/exhibit"
26 |
27 | let route_for_id ?mode id =
28 | match mode with
29 | | Some `List -> "/exhibit/list/" ^ Int.to_string id
30 | | _ -> "/exhibit/" ^ Int.to_string id
31 | ;;
32 |
33 | let route_for ex = route_for_id ex.id
34 |
35 | let get_comments =
36 | [%rapper
37 | get_many
38 | {| SELECT @string{comments.content} from comments
39 | INNER JOIN exhibits on exhibits.id = comments.exhibit_id
40 | WHERE exhibits.id = %int{id} |}]
41 | ;;
42 |
--------------------------------------------------------------------------------
/lib/models/image.ml:
--------------------------------------------------------------------------------
1 | module ImageID = struct
2 | type t = int
3 |
4 | let t =
5 | let encode (t : t) : (int, string) result = Ok t in
6 | let decode (t : int) : (t, string) result = Ok t in
7 | Caqti_type.(custom ~encode ~decode int)
8 | ;;
9 |
10 | let mk (t : int) : t = t
11 | end
12 |
13 | module _ : Rapper.CUSTOM = ImageID
14 |
15 | type t =
16 | { id : ImageID.t
17 | ; data : string
18 | }
19 | [@@deriving model ~table_name:"images"]
20 |
--------------------------------------------------------------------------------
/lib/models/user.ml:
--------------------------------------------------------------------------------
1 | module UserID = struct
2 | type t = int
3 |
4 | let t =
5 | let encode (t : t) : (int, string) result = Ok t in
6 | let decode (t : int) : (t, string) result = Ok t in
7 | Caqti_type.(custom ~encode ~decode int)
8 | ;;
9 |
10 | let mk (t : int) : t = t
11 | end
12 |
13 | module _ : Rapper.CUSTOM = UserID
14 |
15 | type id = UserID.t
16 |
17 | type t =
18 | { id : UserID.t
19 | ; name : string
20 | }
21 | [@@deriving model ~table_name:"users"]
22 |
23 | (* All of these are generated from my deriving macro that I've been working on *)
24 | let _ = create
25 | let _ = create_record
26 | let _ = read
27 | let _ = read_all
28 |
29 | let format { id; name } =
30 | let open Tyxml_html in
31 | div [ h1 [ txt name ]; p [ txt (Printf.sprintf "ID: %d" id) ] ]
32 | |> Format.asprintf "%a" (Tyxml.Html.pp_elt ())
33 | ;;
34 |
35 | let login_user request name password =
36 | let query =
37 | [%rapper
38 | get_one
39 | {| SELECT @int{id}, @string{name} FROM users where name = %string{name} and password = %string{password} |}
40 | record_out]
41 | in
42 | Dream.sql request (query ~name ~password)
43 | ;;
44 |
45 | let handle_get request =
46 | let id = Dream.param request "id" in
47 | let%lwt resource = Dream.sql request (read ~id:(int_of_string id)) in
48 | match resource with
49 | | Ok (Some resource) -> Dream.html @@ format resource
50 | | _ -> Dream.empty `Not_Found
51 | ;;
52 |
--------------------------------------------------------------------------------
/lib/resource.ml:
--------------------------------------------------------------------------------
1 | open Lwt_result.Syntax
2 |
3 | module type RESOURCE = sig
4 | type t
5 | type data
6 |
7 | val create_query : data -> Database.t -> (int, Caqti_error.t) Lwt_result.t
8 | val read_query : id:int -> Database.t -> (t option, Caqti_error.t) Lwt_result.t
9 | val format : t -> [ `Div ] Tyxml_html.elt
10 | end
11 |
12 | module Resource (T : RESOURCE) = struct
13 | type t = T.t
14 | type data = T.data
15 |
16 | let read (id : int) (db : Database.t) = T.read_query ~id db
17 |
18 | let create (data : T.data) db =
19 | let* id = T.create_query data db in
20 | read id db |> Lwt_result.map Option.get
21 | ;;
22 |
23 | let format t = T.format t |> Format.asprintf "%a" (Tyxml.Html.pp_elt ())
24 |
25 | let route_get request =
26 | let id = Dream.param request "id" in
27 | let%lwt resource = Dream.sql request (read (int_of_string id)) in
28 | match resource with
29 | | Ok (Some resource) -> Dream.html @@ format resource
30 | | _ -> Dream.empty `Not_Found
31 | ;;
32 | end
33 |
--------------------------------------------------------------------------------
/lib/sql_setup.eml.ml:
--------------------------------------------------------------------------------
1 | open Base
2 |
3 | module type DB = Caqti_lwt.CONNECTION
4 |
5 | module T = Caqti_type
6 |
7 | let exec_strs (module Db : DB) stmts =
8 | Lwt_list.iter_s
9 | (fun stmt ->
10 | let query =
11 | let open Caqti_request.Infix in
12 | (T.unit ->. T.unit) stmt
13 | in
14 | let%lwt unit_or_error = Db.exec query () in
15 | Caqti_lwt.or_fail unit_or_error)
16 | stmts
17 | ;;
18 |
19 | let migrate (module Db : DB) =
20 | let unwrap x =
21 | match x with
22 | | Ok x -> x
23 | | _ -> failwith "nope"
24 | in
25 | Fpath.v "./tables/"
26 | |> Bos.OS.Dir.contents
27 | |> unwrap
28 | |> List.sort ~compare:(fun a b ->
29 | String.compare (Fpath.to_string a) (Fpath.to_string b))
30 | |> List.map ~f:(fun migration -> Bos.OS.File.read migration |> unwrap)
31 | |> exec_strs (module Db)
32 | ;;
33 |
34 | let populate (module Db : DB) =
35 | let queries =
36 | [ {| INSERT INTO users (name, password) VALUES
37 | ('teej_dv', '12345'),
38 | ('opti', 'hunter2'),
39 | ('nightshadedude', 'ilovepiq') |}
40 | ; {| INSERT INTO exhibits (user_id, content) VALUES
41 | (1, 'This is an example exhibit'),
42 | (1, 'A middle exhibit'),
43 | (2, 'This is another one') |}
44 | ; {| INSERT INTO comments (content, exhibit_id) VALUES
45 | ('Wow, first try?', 1),
46 | ('Opti is a great teacher', 1) |}
47 | ]
48 | in
49 | exec_strs (module Db) queries
50 | ;;
51 |
--------------------------------------------------------------------------------
/ohtml.opam:
--------------------------------------------------------------------------------
1 | # This file is generated by dune, edit dune-project instead
2 | opam-version: "2.0"
3 | synopsis: "OhtML (Oatmeal) is a web framework where you get to have fun."
4 | description: "No, seriously. The goal really is to have fun"
5 | maintainer: ["TJ DeVries"]
6 | authors: ["TJ DeVries"]
7 | license: "MIT"
8 | homepage: "https://github.com/tjdevries/OhtML"
9 | bug-reports: "https://github.com/tjdevries/OhtML/issues"
10 | depends: [
11 | "ocaml" {>= "5.0"}
12 | "dune" {>= "3.7"}
13 | "dream"
14 | "dream-livereload"
15 | "tyxml"
16 | "bos"
17 | "base"
18 | "ppx_let"
19 | "fmt"
20 | "lwt"
21 | "caqti"
22 | "caqti-lwt"
23 | "caqti-dynload"
24 | "caqti-driver-sqlite3"
25 | "ppx_rapper"
26 | "ppx_rapper_lwt"
27 | "odoc" {with-doc}
28 | ]
29 | build: [
30 | ["dune" "subst"] {dev}
31 | [
32 | "dune"
33 | "build"
34 | "-p"
35 | name
36 | "-j"
37 | jobs
38 | "@install"
39 | "@runtest" {with-test}
40 | "@doc" {with-doc}
41 | ]
42 | ]
43 | dev-repo: "git+https://github.com/tjdevries/OhtML.git"
44 |
--------------------------------------------------------------------------------
/ohtml.opam.locked:
--------------------------------------------------------------------------------
1 | opam-version: "2.0"
2 | synopsis: "opam-monorepo generated lockfile"
3 | maintainer: "opam-monorepo"
4 | depends: [
5 | "angstrom" {= "0.15.0" & ?vendor}
6 | "astring" {= "0.8.5+dune" & ?vendor}
7 | "base" {= "v0.15.1" & ?vendor}
8 | "base-bigarray" {= "base"}
9 | "base-bytes" {= "base+dune" & ?vendor}
10 | "base-domains" {= "base"}
11 | "base-nnp" {= "base"}
12 | "base-threads" {= "base"}
13 | "base-unix" {= "base"}
14 | "base64" {= "3.5.1" & ?vendor}
15 | "bigarray-compat" {= "1.1.0" & ?vendor}
16 | "bigarray-overlap" {= "0.2.1" & ?vendor}
17 | "bigstringaf" {= "0.9.1" & ?vendor}
18 | "bos" {= "0.2.1+dune" & ?vendor}
19 | "camlp-streams" {= "5.0.1" & ?vendor}
20 | "caqti" {= "1.9.0" & ?vendor}
21 | "caqti-driver-sqlite3" {= "1.9.0" & ?vendor}
22 | "caqti-dynload" {= "1.3.0" & ?vendor}
23 | "caqti-lwt" {= "1.9.0" & ?vendor}
24 | "cmdliner" {= "1.1.1+dune" & ?vendor}
25 | "conf-libev" {= "4-12"}
26 | "conf-libssl" {= "4"}
27 | "conf-pkg-config" {= "2"}
28 | "conf-sqlite3" {= "1"}
29 | "cppo" {= "1.6.9" & ?vendor}
30 | "csexp" {= "1.5.2" & ?vendor}
31 | "cstruct" {= "6.2.0" & ?vendor}
32 | "ctypes" {= "0.20.1+dune" & ?vendor}
33 | "ctypes-foreign" {= "0.20.1+dune" & ?vendor}
34 | "digestif" {= "1.1.4" & ?vendor}
35 | "dream" {= "1.0.0~alpha5" & ?vendor}
36 | "dream-httpaf" {= "1.0.0~alpha2" & ?vendor}
37 | "dream-livereload" {= "0.2.0" & ?vendor}
38 | "dream-pure" {= "1.0.0~alpha2" & ?vendor}
39 | "dune" {= "3.7.1"}
40 | "dune-configurator" {= "3.7.1" & ?vendor}
41 | "duration" {= "0.2.1" & ?vendor}
42 | "eqaf" {= "0.9" & ?vendor}
43 | "faraday" {= "0.8.2" & ?vendor}
44 | "faraday-lwt" {= "0.8.2" & ?vendor}
45 | "faraday-lwt-unix" {= "0.8.2" & ?vendor}
46 | "findlib" {= "1.9.5+dune" & ?vendor}
47 | "fmt" {= "0.9.0+dune" & ?vendor}
48 | "fpath" {= "0.7.3+dune" & ?vendor}
49 | "graphql" {= "0.14.0" & ?vendor}
50 | "graphql-lwt" {= "0.14.0" & ?vendor}
51 | "graphql_parser" {= "0.14.0" & ?vendor}
52 | "hmap" {= "0.8.1+dune" & ?vendor}
53 | "integers" {= "0.7.0" & ?vendor}
54 | "ke" {= "0.6" & ?vendor}
55 | "lambdasoup" {= "1.0.0" & ?vendor}
56 | "logs" {= "0.7.0+dune2" & ?vendor}
57 | "lwt" {= "5.6.1" & ?vendor}
58 | "lwt_ppx" {= "2.1.0" & ?vendor}
59 | "lwt_ssl" {= "1.2.0" & ?vendor}
60 | "magic-mime" {= "1.3.0" & ?vendor}
61 | "markup" {= "1.0.3" & ?vendor}
62 | "menhir" {= "20230415" & ?vendor}
63 | "menhirLib" {= "20230415" & ?vendor}
64 | "menhirSdk" {= "20230415" & ?vendor}
65 | "mirage-clock" {= "4.2.0" & ?vendor}
66 | "mirage-crypto" {= "0.11.1" & ?vendor}
67 | "mirage-crypto-rng" {= "0.11.1" & ?vendor}
68 | "mirage-crypto-rng-lwt" {= "0.11.1" & ?vendor}
69 | "mtime" {= "2.0.0+dune" & ?vendor}
70 | "multipart_form" {= "0.5.0" & ?vendor}
71 | "multipart_form-lwt" {= "0.5.0" & ?vendor}
72 | "ocaml" {= "5.0.0"}
73 | "ocaml-base-compiler" {= "5.0.0"}
74 | "ocaml-compiler-libs" {= "v0.12.4" & ?vendor}
75 | "ocaml-config" {= "3"}
76 | "ocaml-options-vanilla" {= "1"}
77 | "ocaml-syntax-shims" {= "1.0.0" & ?vendor}
78 | "ocamlfind" {= "1.9.5+dune" & ?vendor}
79 | "ocplib-endian" {= "1.2" & ?vendor}
80 | "pecu" {= "0.6" & ?vendor}
81 | "pg_query" {= "0.9.8" & ?vendor}
82 | "ppx_derivers" {= "1.2.1" & ?vendor}
83 | "ppx_deriving" {= "5.2.1" & ?vendor}
84 | "ppx_here" {= "v0.15.0" & ?vendor}
85 | "ppx_let" {= "v0.15.0" & ?vendor}
86 | "ppx_rapper" {= "3.1.0" & ?vendor}
87 | "ppx_rapper_lwt" {= "3.1.0" & ?vendor}
88 | "ppxlib" {= "0.29.1" & ?vendor}
89 | "prettym" {= "0.0.3" & ?vendor}
90 | "psq" {= "0.2.1" & ?vendor}
91 | "ptime" {= "1.1.0+dune" & ?vendor}
92 | "re" {= "1.10.4" & ?vendor}
93 | "result" {= "1.5" & ?vendor}
94 | "rresult" {= "0.7.0+dune" & ?vendor}
95 | "seq" {= "base+dune" & ?vendor}
96 | "sexplib0" {= "v0.15.1" & ?vendor}
97 | "sqlite3" {= "5.1.0" & ?vendor}
98 | "ssl" {= "0.5.13" & ?vendor}
99 | "stdlib-shims" {= "0.3.0" & ?vendor}
100 | "stringext" {= "1.6.0" & ?vendor}
101 | "tyxml" {= "4.5.0" & ?vendor}
102 | "uchar" {= "0.0.2+dune2" & ?vendor}
103 | "unstrctrd" {= "0.3" & ?vendor}
104 | "uri" {= "4.2.0" & ?vendor}
105 | "uutf" {= "1.0.3+dune" & ?vendor}
106 | "yojson" {= "2.0.2" & ?vendor}
107 | ]
108 | depexts: [
109 | ["database/sqlite3"] {os = "openbsd"}
110 | ["devel/pkgconf"] {os = "openbsd"}
111 | ["libev"] {os = "freebsd"}
112 | ["libev"] {os = "openbsd"}
113 | ["libev"] {os-distribution = "nixos"}
114 | ["libev"] {os-family = "arch"}
115 | ["libev"] {os = "macos" & os-distribution = "homebrew"}
116 | ["libev-dev"] {os-family = "alpine"}
117 | ["libev-dev"] {os-family = "debian"}
118 | ["libev-dev"] {os-family = "ubuntu"}
119 | ["libev-devel"] {os-distribution = "centos"}
120 | ["libev-devel"] {os-distribution = "fedora"}
121 | ["libev-devel"] {os-distribution = "rhel"}
122 | ["libev-devel"] {os-family = "suse"}
123 | ["libev-devel"] {os-distribution = "ol" & os-version >= "8"}
124 | ["libffi"] {os = "macos" & os-distribution = "homebrew"}
125 | ["libffi"] {os = "macos" & os-distribution = "macports"}
126 | ["libffi"] {os = "win32" & os-distribution = "cygwinports"}
127 | ["libffi-dev"] {os-distribution = "alpine"}
128 | ["libffi-dev"] {os-distribution = "debian"}
129 | ["libffi-dev"] {os-distribution = "ubuntu"}
130 | ["libffi-devel"] {os-distribution = "centos"}
131 | ["libffi-devel"] {os-distribution = "fedora"}
132 | ["libffi-devel"] {os-distribution = "oraclelinux"}
133 | ["libffi-devel"] {os-family = "suse"}
134 | ["libopenssl-devel"] {os-family = "suse"}
135 | ["libsqlite3-dev"] {os-family = "debian"}
136 | ["libssl-dev"] {os-family = "debian"}
137 | ["libssl-dev"] {os-family = "ubuntu"}
138 | ["openssl"] {os-distribution = "homebrew"}
139 | ["openssl"] {os-distribution = "macports"}
140 | ["openssl"] {os-distribution = "nixos"}
141 | ["openssl"] {os-family = "arch"}
142 | ["openssl"] {os = "win32" & os-distribution = "cygwinports"}
143 | ["openssl-dev"] {os-family = "alpine"}
144 | ["openssl-devel"] {os-distribution = "centos"}
145 | ["openssl-devel"] {os-distribution = "fedora"}
146 | ["openssl-devel"] {os-distribution = "ol"}
147 | ["pkg-config"] {os-family = "debian"}
148 | ["pkg-config"] {os = "macos" & os-distribution = "homebrew"}
149 | ["pkgconf"] {os = "freebsd"}
150 | ["pkgconf"] {os-distribution = "alpine"}
151 | ["pkgconf"] {os-distribution = "arch"}
152 | ["pkgconf-pkg-config"] {os-distribution = "fedora"}
153 | ["pkgconf-pkg-config"] {os-distribution = "mageia"}
154 | ["pkgconf-pkg-config"] {os-distribution = "centos" & os-version >= "8"}
155 | ["pkgconf-pkg-config"] {os-distribution = "ol" & os-version >= "8"}
156 | ["pkgconf-pkg-config"] {os-distribution = "rhel" & os-version >= "8"}
157 | ["pkgconfig"] {os-distribution = "nixos"}
158 | ["pkgconfig"] {os = "macos" & os-distribution = "macports"}
159 | ["pkgconfig"] {os-distribution = "centos" & os-version <= "7"}
160 | ["pkgconfig"] {os-distribution = "ol" & os-version <= "7"}
161 | ["pkgconfig"] {os-distribution = "rhel" & os-version <= "7"}
162 | ["sqlite"] {os-distribution = "nixos"}
163 | ["sqlite"] {os = "macos" & os-distribution = "homebrew"}
164 | ["sqlite-dev"] {os-distribution = "alpine"}
165 | ["sqlite-devel"] {os-distribution = "centos"}
166 | ["sqlite-devel"] {os-distribution = "fedora"}
167 | ["sqlite-devel"] {os-distribution = "ol"}
168 | ["sqlite-devel"] {os-distribution = "rhel"}
169 | ["sqlite3"] {os = "freebsd"}
170 | ["sqlite3"] {os = "macos" & os-distribution = "macports"}
171 | ["sqlite3"] {os = "win32" & os-distribution = "cygwinports"}
172 | ["sqlite3-devel"] {os-family = "suse"}
173 | ["system:pkgconf"] {os = "win32" & os-distribution = "cygwinports"}
174 | ]
175 | pin-depends: [
176 | [
177 | "angstrom.0.15.0"
178 | "https://github.com/inhabitedtype/angstrom/archive/0.15.0.tar.gz"
179 | ]
180 | [
181 | "astring.0.8.5+dune"
182 | "https://github.com/dune-universe/astring/archive/v0.8.5+dune.tar.gz"
183 | ]
184 | [
185 | "base.v0.15.1"
186 | "https://github.com/janestreet/base/archive/refs/tags/v0.15.1.tar.gz"
187 | ]
188 | [
189 | "base-bytes.base+dune"
190 | "https://github.com/kit-ty-kate/bytes/archive/v0.1.0.tar.gz"
191 | ]
192 | [
193 | "base64.3.5.1"
194 | "https://github.com/mirage/ocaml-base64/releases/download/v3.5.1/base64-3.5.1.tbz"
195 | ]
196 | [
197 | "bigarray-compat.1.1.0"
198 | "https://github.com/mirage/bigarray-compat/releases/download/v1.1.0/bigarray-compat-1.1.0.tbz"
199 | ]
200 | [
201 | "bigarray-overlap.0.2.1"
202 | "https://github.com/dinosaure/overlap/releases/download/v0.2.1/bigarray-overlap-0.2.1.tbz"
203 | ]
204 | [
205 | "bigstringaf.0.9.1"
206 | "https://github.com/inhabitedtype/bigstringaf/archive/0.9.1.tar.gz"
207 | ]
208 | [
209 | "bos.0.2.1+dune"
210 | "https://github.com/dune-universe/bos/releases/download/v0.2.1%2Bdune/bos-0.2.1.dune.tbz"
211 | ]
212 | [
213 | "camlp-streams.5.0.1"
214 | "https://github.com/ocaml/camlp-streams/archive/v5.0.1.tar.gz"
215 | ]
216 | [
217 | "caqti.1.9.0"
218 | "https://github.com/paurkedal/ocaml-caqti/releases/download/v1.9.0/caqti-v1.9.0.tbz"
219 | ]
220 | [
221 | "caqti-driver-sqlite3.1.9.0"
222 | "https://github.com/paurkedal/ocaml-caqti/releases/download/v1.9.0/caqti-v1.9.0.tbz"
223 | ]
224 | [
225 | "caqti-dynload.1.3.0"
226 | "https://github.com/paurkedal/ocaml-caqti/releases/download/v1.9.0/caqti-v1.9.0.tbz"
227 | ]
228 | [
229 | "caqti-lwt.1.9.0"
230 | "https://github.com/paurkedal/ocaml-caqti/releases/download/v1.9.0/caqti-v1.9.0.tbz"
231 | ]
232 | [
233 | "cmdliner.1.1.1+dune"
234 | "https://erratique.ch/software/cmdliner/releases/cmdliner-1.1.1.tbz"
235 | ]
236 | [
237 | "cppo.1.6.9"
238 | "https://github.com/ocaml-community/cppo/archive/v1.6.9.tar.gz"
239 | ]
240 | [
241 | "csexp.1.5.2"
242 | "https://github.com/ocaml-dune/csexp/releases/download/1.5.2/csexp-1.5.2.tbz"
243 | ]
244 | [
245 | "cstruct.6.2.0"
246 | "https://github.com/mirage/ocaml-cstruct/releases/download/v6.2.0/cstruct-6.2.0.tbz"
247 | ]
248 | [
249 | "ctypes.0.20.1+dune"
250 | "https://github.com/dune-universe/ocaml-ctypes/releases/download/0.20.1%2Bdune/ctypes-foreign-0.20.1.dune.tbz"
251 | ]
252 | [
253 | "ctypes-foreign.0.20.1+dune"
254 | "https://github.com/dune-universe/ocaml-ctypes/releases/download/0.20.1%2Bdune/ctypes-foreign-0.20.1.dune.tbz"
255 | ]
256 | [
257 | "digestif.1.1.4"
258 | "https://github.com/mirage/digestif/releases/download/v1.1.4/digestif-1.1.4.tbz"
259 | ]
260 | [
261 | "dream.1.0.0~alpha5"
262 | "git+https://github.com/aantron/dream.git#006196fa0c957ea4f4aa3e949803b09cc1b3010b"
263 | ]
264 | [
265 | "dream-httpaf.1.0.0~alpha2"
266 | "git+https://github.com/aantron/dream.git#006196fa0c957ea4f4aa3e949803b09cc1b3010b"
267 | ]
268 | [
269 | "dream-livereload.0.2.0"
270 | "https://github.com/tmattio/dream-livereload/releases/download/0.2.0/dream-livereload-0.2.0.tbz"
271 | ]
272 | [
273 | "dream-pure.1.0.0~alpha2"
274 | "git+https://github.com/aantron/dream.git#006196fa0c957ea4f4aa3e949803b09cc1b3010b"
275 | ]
276 | [
277 | "dune-configurator.3.7.1"
278 | "https://github.com/ocaml/dune/releases/download/3.7.1/dune-3.7.1.tbz"
279 | ]
280 | [
281 | "duration.0.2.1"
282 | "https://github.com/hannesm/duration/releases/download/v0.2.1/duration-0.2.1.tbz"
283 | ]
284 | [
285 | "eqaf.0.9"
286 | "https://github.com/mirage/eqaf/releases/download/v0.9/eqaf-0.9.tbz"
287 | ]
288 | [
289 | "faraday.0.8.2"
290 | "https://github.com/inhabitedtype/faraday/archive/0.8.2.tar.gz"
291 | ]
292 | [
293 | "faraday-lwt.0.8.2"
294 | "https://github.com/inhabitedtype/faraday/archive/0.8.2.tar.gz"
295 | ]
296 | [
297 | "faraday-lwt-unix.0.8.2"
298 | "https://github.com/inhabitedtype/faraday/archive/0.8.2.tar.gz"
299 | ]
300 | [
301 | "findlib.1.9.5+dune"
302 | "https://github.com/dune-universe/lib-findlib/releases/download/1.9.5%2Bdune/findlib-1.9.5.dune.tbz"
303 | ]
304 | [
305 | "fmt.0.9.0+dune"
306 | "https://github.com/dune-universe/fmt/releases/download/v0.9.0%2Bdune/fmt-0.9.0.dune.tbz"
307 | ]
308 | [
309 | "fpath.0.7.3+dune"
310 | "https://github.com/dune-universe/fpath/archive/v0.7.3+dune.tar.gz"
311 | ]
312 | [
313 | "graphql.0.14.0"
314 | "https://github.com/andreas/ocaml-graphql-server/releases/download/0.14.0/graphql-0.14.0.tbz"
315 | ]
316 | [
317 | "graphql-lwt.0.14.0"
318 | "https://github.com/andreas/ocaml-graphql-server/releases/download/0.14.0/graphql-0.14.0.tbz"
319 | ]
320 | [
321 | "graphql_parser.0.14.0"
322 | "https://github.com/andreas/ocaml-graphql-server/releases/download/0.14.0/graphql-0.14.0.tbz"
323 | ]
324 | [
325 | "hmap.0.8.1+dune"
326 | "https://github.com/dune-universe/hmap/releases/download/v0.8.1%2Bdune/hmap-0.8.1.dune.tbz"
327 | ]
328 | [
329 | "integers.0.7.0"
330 | "https://github.com/yallop/ocaml-integers/archive/0.7.0.tar.gz"
331 | ]
332 | ["ke.0.6" "https://github.com/mirage/ke/releases/download/v0.6/ke-0.6.tbz"]
333 | [
334 | "lambdasoup.1.0.0"
335 | "https://github.com/aantron/lambdasoup/archive/1.0.0.tar.gz"
336 | ]
337 | [
338 | "logs.0.7.0+dune2"
339 | "https://github.com/dune-universe/logs/releases/download/v0.7.0%2Bdune2/logs-0.7.0.dune2.tbz"
340 | ]
341 | [
342 | "lwt.5.6.1"
343 | "git+https://github.com/ocsigen/lwt.git#cc05e2bda6c34126a3fd8d150ee7cddb3b8a440b"
344 | ]
345 | [
346 | "lwt_ppx.2.1.0"
347 | "git+https://github.com/ocsigen/lwt.git#cc05e2bda6c34126a3fd8d150ee7cddb3b8a440b"
348 | ]
349 | [
350 | "lwt_ssl.1.2.0"
351 | "https://github.com/ocsigen/lwt_ssl/releases/download/1.2.0/lwt_ssl-1.2.0.tbz"
352 | ]
353 | [
354 | "magic-mime.1.3.0"
355 | "https://github.com/mirage/ocaml-magic-mime/releases/download/v1.3.0/magic-mime-1.3.0.tbz"
356 | ]
357 | [
358 | "markup.1.0.3" "https://github.com/aantron/markup.ml/archive/1.0.3.tar.gz"
359 | ]
360 | [
361 | "menhir.20230415"
362 | "https://gitlab.inria.fr/fpottier/menhir/-/archive/20230415/archive.tar.gz"
363 | ]
364 | [
365 | "menhirLib.20230415"
366 | "https://gitlab.inria.fr/fpottier/menhir/-/archive/20230415/archive.tar.gz"
367 | ]
368 | [
369 | "menhirSdk.20230415"
370 | "https://gitlab.inria.fr/fpottier/menhir/-/archive/20230415/archive.tar.gz"
371 | ]
372 | [
373 | "mirage-clock.4.2.0"
374 | "https://github.com/mirage/mirage-clock/releases/download/v4.2.0/mirage-clock-4.2.0.tbz"
375 | ]
376 | [
377 | "mirage-crypto.0.11.1"
378 | "https://github.com/mirage/mirage-crypto/releases/download/v0.11.1/mirage-crypto-0.11.1.tbz"
379 | ]
380 | [
381 | "mirage-crypto-rng.0.11.1"
382 | "https://github.com/mirage/mirage-crypto/releases/download/v0.11.1/mirage-crypto-0.11.1.tbz"
383 | ]
384 | [
385 | "mirage-crypto-rng-lwt.0.11.1"
386 | "https://github.com/mirage/mirage-crypto/releases/download/v0.11.1/mirage-crypto-0.11.1.tbz"
387 | ]
388 | [
389 | "mtime.2.0.0+dune"
390 | "https://github.com/dune-universe/mtime/releases/download/2.0.0%2Bdune/mtime-2.0.0.dune.tbz"
391 | ]
392 | [
393 | "multipart_form.0.5.0"
394 | "https://github.com/dinosaure/multipart_form/releases/download/v0.5.0/multipart_form-0.5.0.tbz"
395 | ]
396 | [
397 | "multipart_form-lwt.0.5.0"
398 | "https://github.com/dinosaure/multipart_form/releases/download/v0.5.0/multipart_form-0.5.0.tbz"
399 | ]
400 | [
401 | "ocaml-compiler-libs.v0.12.4"
402 | "https://github.com/janestreet/ocaml-compiler-libs/releases/download/v0.12.4/ocaml-compiler-libs-v0.12.4.tbz"
403 | ]
404 | [
405 | "ocaml-syntax-shims.1.0.0"
406 | "https://github.com/ocaml-ppx/ocaml-syntax-shims/releases/download/1.0.0/ocaml-syntax-shims-1.0.0.tbz"
407 | ]
408 | [
409 | "ocamlfind.1.9.5+dune"
410 | "https://github.com/dune-universe/lib-findlib/releases/download/1.9.5%2Bdune/findlib-1.9.5.dune.tbz"
411 | ]
412 | [
413 | "ocplib-endian.1.2"
414 | "https://github.com/OCamlPro/ocplib-endian/archive/refs/tags/1.2.tar.gz"
415 | ]
416 | [
417 | "pecu.0.6"
418 | "https://github.com/mirage/pecu/releases/download/v0.6/pecu-v0.6.tbz"
419 | ]
420 | [
421 | "pg_query.0.9.8"
422 | "git+https://github.com/roddyyaga/pg_query-ocaml.git#9b37b2b2937fe9e1e52f778cd897acef458e8327"
423 | ]
424 | [
425 | "ppx_derivers.1.2.1"
426 | "https://github.com/ocaml-ppx/ppx_derivers/archive/1.2.1.tar.gz"
427 | ]
428 | [
429 | "ppx_deriving.5.2.1"
430 | "https://github.com/ocaml-ppx/ppx_deriving/releases/download/v5.2.1/ppx_deriving-v5.2.1.tbz"
431 | ]
432 | [
433 | "ppx_here.v0.15.0"
434 | "https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_here-v0.15.0.tar.gz"
435 | ]
436 | [
437 | "ppx_let.v0.15.0"
438 | "https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_let-v0.15.0.tar.gz"
439 | ]
440 | [
441 | "ppx_rapper.3.1.0"
442 | "https://github.com/roddyyaga/ppx_rapper/archive/3.1.0.tar.gz"
443 | ]
444 | [
445 | "ppx_rapper_lwt.3.1.0"
446 | "https://github.com/roddyyaga/ppx_rapper/archive/3.1.0.tar.gz"
447 | ]
448 | [
449 | "ppxlib.0.29.1"
450 | "https://github.com/ocaml-ppx/ppxlib/releases/download/0.29.1/ppxlib-0.29.1.tbz"
451 | ]
452 | [
453 | "prettym.0.0.3"
454 | "https://github.com/dinosaure/prettym/releases/download/0.0.3/prettym-0.0.3.tbz"
455 | ]
456 | [
457 | "psq.0.2.1"
458 | "https://github.com/pqwy/psq/releases/download/v0.2.1/psq-0.2.1.tbz"
459 | ]
460 | [
461 | "ptime.1.1.0+dune"
462 | "https://github.com/dune-universe/ptime/releases/download/v1.1.0%2Bdune/ptime-1.1.0.dune.tbz"
463 | ]
464 | [
465 | "re.1.10.4"
466 | "https://github.com/ocaml/ocaml-re/releases/download/1.10.4/re-1.10.4.tbz"
467 | ]
468 | [
469 | "result.1.5"
470 | "https://github.com/janestreet/result/releases/download/1.5/result-1.5.tbz"
471 | ]
472 | [
473 | "rresult.0.7.0+dune"
474 | "https://github.com/dune-universe/rresult/releases/download/v0.7.0%2Bdune/rresult-0.7.0.dune.tbz"
475 | ]
476 | ["seq.base+dune" "https://github.com/c-cube/seq/archive/0.2.2.tar.gz"]
477 | [
478 | "sexplib0.v0.15.1"
479 | "https://github.com/janestreet/sexplib0/archive/refs/tags/v0.15.1.tar.gz"
480 | ]
481 | [
482 | "sqlite3.5.1.0"
483 | "https://github.com/mmottl/sqlite3-ocaml/releases/download/5.1.0/sqlite3-5.1.0.tbz"
484 | ]
485 | [
486 | "ssl.0.5.13"
487 | "https://github.com/savonet/ocaml-ssl/releases/download/0.5.13/ssl-0.5.13.tbz"
488 | ]
489 | [
490 | "stdlib-shims.0.3.0"
491 | "https://github.com/ocaml/stdlib-shims/releases/download/0.3.0/stdlib-shims-0.3.0.tbz"
492 | ]
493 | [
494 | "stringext.1.6.0"
495 | "https://github.com/rgrinberg/stringext/releases/download/1.6.0/stringext-1.6.0.tbz"
496 | ]
497 | [
498 | "tyxml.4.5.0"
499 | "https://github.com/ocsigen/tyxml/releases/download/4.5.0/tyxml-4.5.0.tbz"
500 | ]
501 | [
502 | "uchar.0.0.2+dune2"
503 | "https://github.com/dune-universe/uchar/releases/download/v0.0.2%2Bdune2/uchar-0.0.2.dune2.tbz"
504 | ]
505 | [
506 | "unstrctrd.0.3"
507 | "https://github.com/dinosaure/unstrctrd/releases/download/v0.3/unstrctrd-v0.3.tbz"
508 | ]
509 | [
510 | "uri.4.2.0"
511 | "https://github.com/mirage/ocaml-uri/releases/download/v4.2.0/uri-v4.2.0.tbz"
512 | ]
513 | [
514 | "uutf.1.0.3+dune"
515 | "https://github.com/dune-universe/uutf/releases/download/v1.0.3%2Bdune/uutf-1.0.3.dune.tbz"
516 | ]
517 | [
518 | "yojson.2.0.2"
519 | "https://github.com/ocaml-community/yojson/releases/download/2.0.2/yojson-2.0.2.tbz"
520 | ]
521 | ]
522 | x-opam-monorepo-duniverse-dirs: [
523 | [
524 | "https://erratique.ch/software/cmdliner/releases/cmdliner-1.1.1.tbz"
525 | "cmdliner"
526 | [
527 | "sha512=5478ad833da254b5587b3746e3a8493e66e867a081ac0f653a901cc8a7d944f66e4387592215ce25d939be76f281c4785702f54d4a74b1700bc8838a62255c9e"
528 | ]
529 | ]
530 | [
531 | "https://github.com/OCamlPro/ocplib-endian/archive/refs/tags/1.2.tar.gz"
532 | "ocplib-endian"
533 | [
534 | "md5=8d5492eeb7c6815ade72a7415ea30949"
535 | "sha512=2e70be5f3d6e377485c60664a0e235c3b9b24a8d6b6a03895d092c6e40d53810bfe1f292ee69e5181ce6daa8a582bfe3d59f3af889f417134f658812be5b8b85"
536 | ]
537 | ]
538 | [
539 | "git+https://github.com/aantron/dream.git#006196fa0c957ea4f4aa3e949803b09cc1b3010b"
540 | "dream"
541 | ]
542 | [
543 | "https://github.com/aantron/lambdasoup/archive/1.0.0.tar.gz"
544 | "lambdasoup"
545 | ["md5=35d2d399d1033c0cc081b80dd1a04cc7"]
546 | ]
547 | [
548 | "https://github.com/aantron/markup.ml/archive/1.0.3.tar.gz"
549 | "markup"
550 | ["md5=3609724f5408dff41b1cb43107bc24ef"]
551 | ]
552 | [
553 | "https://github.com/andreas/ocaml-graphql-server/releases/download/0.14.0/graphql-0.14.0.tbz"
554 | "ocaml-graphql-server"
555 | [
556 | "sha256=bf8bf5b9e17e355ecbbd82158a769fe2b138e746753fd3a23008ada3afcd1c06"
557 | "sha512=1d303d9ab67faecea8081f007b3696e36033aa65eba0854f50067b4d667d9a9ad185ad949371790a03509cb31bf6356b75c58f3066da9c35d82e620df5780185"
558 | ]
559 | ]
560 | [
561 | "https://github.com/c-cube/seq/archive/0.2.2.tar.gz"
562 | "seq"
563 | [
564 | "md5=9033e02283aa3bde9f97f24e632902e3"
565 | "sha512=cab0eb4cb6d9788b7cbd7acbefefc15689d706c97ff7f75dd97faf3c21e466af4d0ff110541a24729db587e7172b1a30a3c2967e17ec2e49cbd923360052c07c"
566 | ]
567 | ]
568 | [
569 | "https://github.com/dinosaure/multipart_form/releases/download/v0.5.0/multipart_form-0.5.0.tbz"
570 | "multipart_form"
571 | [
572 | "sha256=a8a36c1c0e2873ba1b3bd32ccdfb8fb6766e06612e52e36b3077a6a296a88a64"
573 | "sha512=f1e95b201e5474687e31bb132ca17ab9799d9957cbc91d0d7fdc160bbcfb2b6f6a77e37fba802c5a41339cb699987667e029672d58096ebfd06fd932b352bd46"
574 | ]
575 | ]
576 | [
577 | "https://github.com/dinosaure/overlap/releases/download/v0.2.1/bigarray-overlap-0.2.1.tbz"
578 | "overlap"
579 | [
580 | "sha256=2f520ac470054e335883eba9254bf28b6676ddb57753cfb58b22cf84ae1a66e0"
581 | "sha512=223f15f815cd944cf2e9955ed9e3cf5608a9db36103e9bb017c2fe452dfb319908228b419f416f6239b8562208027068b7c44e8fb4be6c6a7858ecba540d5439"
582 | ]
583 | ]
584 | [
585 | "https://github.com/dinosaure/prettym/releases/download/0.0.3/prettym-0.0.3.tbz"
586 | "prettym"
587 | [
588 | "sha256=9170f1a11ade7f4d98a584a5be52bb6b91415f971c6e75894331b46b18b98f09"
589 | "sha512=ccb5985daedfb6cae74192090644e81c525df3e0653bb06492f836ca4291275d3ce75079237574200ecab8dacf62304521592d4baebbe4b0d17277b5e200c6a8"
590 | ]
591 | ]
592 | [
593 | "https://github.com/dinosaure/unstrctrd/releases/download/v0.3/unstrctrd-v0.3.tbz"
594 | "unstrctrd"
595 | [
596 | "sha256=7355b1e9a6b977608641c88f9f136fff309ef64e174818c7ccadc8a2a78ded5b"
597 | "sha512=ea2289a331b08523ffcd135e03a4e9fbbc27b46496a1b7dbdd95a0dad57f83cc9b59393ff9a14e557952ad466a2608e60801365993cb22ec79c4650831b620ab"
598 | ]
599 | ]
600 | [
601 | "https://github.com/dune-universe/astring/archive/v0.8.5+dune.tar.gz"
602 | "astring"
603 | [
604 | "sha256=11327c202fd0115f3a2bf7710c9c603b979a32ba9b16c1a64ba155857233acc8"
605 | ]
606 | ]
607 | [
608 | "https://github.com/dune-universe/bos/releases/download/v0.2.1%2Bdune/bos-0.2.1.dune.tbz"
609 | "bos"
610 | [
611 | "sha256=c6a34311946ff906824cedc2d12825ee9ad73b73bfa1581fb8100d6fc3dd5c35"
612 | "sha512=5a1422809050dfbebab9691f29109e8219e27ecc4bc50c2eb714dc59036811936e9c5860b13583ab0ba7c15a00ee5b515af25642cdc312a4814076d8e76e3fd7"
613 | ]
614 | ]
615 | [
616 | "https://github.com/dune-universe/fmt/releases/download/v0.9.0%2Bdune/fmt-0.9.0.dune.tbz"
617 | "fmt"
618 | [
619 | "sha256=844ce674b3146aaf9c14088a0b817cef10c7152054d3cc984543463da978ff81"
620 | "sha512=27765423f43bdfbbdee50906faad14ecf653aaf2fdfc4db6b94791460aa32f3a3490b9d0c1a04aa3ecb0ac4333ea7ce5054251a67a0d67b64f3eb6d737afbf93"
621 | ]
622 | ]
623 | [
624 | "https://github.com/dune-universe/fpath/archive/v0.7.3+dune.tar.gz"
625 | "fpath"
626 | [
627 | "sha256=792ecf88d2a311596106e30775864629558ed0c2d0501590fda55f363dbb6ebc"
628 | ]
629 | ]
630 | [
631 | "https://github.com/dune-universe/hmap/releases/download/v0.8.1%2Bdune/hmap-0.8.1.dune.tbz"
632 | "hmap"
633 | [
634 | "sha256=e1c87e4407c285caab596200b39983e43e932f6d67120087e2652548bcbb5a87"
635 | "sha512=4bfba9c29a7751d0f19495f8c090400b556923611bf558ee6c5a8ea781499373865b176a84355c0b047cab8efbd1692ad45198422cc5267d36f21a137ce15364"
636 | ]
637 | ]
638 | [
639 | "https://github.com/dune-universe/lib-findlib/releases/download/1.9.5%2Bdune/findlib-1.9.5.dune.tbz"
640 | "lib-findlib"
641 | [
642 | "sha256=5242e5a0cfb2af52d6b596767513d80d2a3588608b366c759f9df0841736a228"
643 | "sha512=1b39aedd0cbf623acb9abca88e65e5114f44524f3443f45def184663da76db515e791bee625282a69d20d88e7bc7d4522dbd142e551e09bd7a4cf99d1eabfe95"
644 | ]
645 | ]
646 | [
647 | "https://github.com/dune-universe/logs/releases/download/v0.7.0%2Bdune2/logs-0.7.0.dune2.tbz"
648 | "logs"
649 | [
650 | "sha256=ae2f76b6bb42122371041d389d0d4348346a79b38ffbb7c20d08e85df2fedf76"
651 | "sha512=4c1fdc23c5f9709d50fa1ee518e2ec4cf1a35fb1cedf466bcc849ae47c113b317db2bf95c788d48faacb67952d942d4b378904e3c37e71ef7babb56e2f11ce8b"
652 | ]
653 | ]
654 | [
655 | "https://github.com/dune-universe/mtime/releases/download/2.0.0%2Bdune/mtime-2.0.0.dune.tbz"
656 | "mtime"
657 | [
658 | "sha256=7dd6d0ba21acd07c2c76d6519a58c09e420af0fba57cfd8dd8ce08535db03a54"
659 | "sha512=75942aaad6e25d97b11e0038effc3bed980d336435bffbaecb67368e83299b17d77db92a79d9a010f5961fc8ede7ae346fa91182e6c002f964ce9e0944b6a9ac"
660 | ]
661 | ]
662 | [
663 | "https://github.com/dune-universe/ocaml-ctypes/releases/download/0.20.1%2Bdune/ctypes-foreign-0.20.1.dune.tbz"
664 | "ocaml-ctypes"
665 | [
666 | "sha256=fd4ecf06c1d90931408c26708c21b7eb07bf703fdf06a793431451b7a1e8676f"
667 | "sha512=241f2cf9b4dc9d352c7b5d487667dbef0d7708d025d9ae0adaab7389975ed76b67ef923d8041e4d6e3ce895d904f68ddded7812d514e117c32c4aae5fd9b5e2a"
668 | ]
669 | ]
670 | [
671 | "https://github.com/dune-universe/ptime/releases/download/v1.1.0%2Bdune/ptime-1.1.0.dune.tbz"
672 | "ptime"
673 | [
674 | "sha256=9aa6645808ce539eeafe4eaf781d6d0ae5639c90592cee31d15cf9363acf9234"
675 | "sha512=0d037fc8f11c25b407f5501bd614dbbadb83fd6dce2fff5c0eeae27a9cd616f69f399f0cb3a04a2b0588cf1866cb16d6d1ae599791509cb995a92d3ae33ad1a0"
676 | ]
677 | ]
678 | [
679 | "https://github.com/dune-universe/rresult/releases/download/v0.7.0%2Bdune/rresult-0.7.0.dune.tbz"
680 | "rresult"
681 | [
682 | "sha256=3726c0ddf709e1886ef9adae83bf3696fa65466cc675d2494fa6ea9da9945a9f"
683 | "sha512=e29d1a41fca85a301df370183740d89c6a23ceb7fa530e8ba3693917032d5784b7899b6f713fd5f66d49c3426811a65465f5709af23b3f9120017f94cd9a448e"
684 | ]
685 | ]
686 | [
687 | "https://github.com/dune-universe/uchar/releases/download/v0.0.2%2Bdune2/uchar-0.0.2.dune2.tbz"
688 | "uchar"
689 | [
690 | "sha256=de4d427925ef25af0a605f3a91fee1c00b402bd2bea7849d05acbb225252983f"
691 | "sha512=a568b7a040f3c7ebb5c15d0a9e5f84ce9c80307c132159e3f139f96db16e89e0cb4bbaadefea55557b6b9f26b79238e45223806570c6f2b95d5045ff71413d23"
692 | ]
693 | ]
694 | [
695 | "https://github.com/dune-universe/uutf/releases/download/v1.0.3%2Bdune/uutf-1.0.3.dune.tbz"
696 | "uutf"
697 | [
698 | "sha256=a207104302c6025b32377e6b4f046a037c56e3de12ce7eacd44c2f31ce71649d"
699 | "sha512=7f8904668a37f39a0a61d63539c0afb55d5127e57e0b4ea7ce944216d8d299e44b0f13972ad55f973c93a659ee0f97cf0f1421a7012a15be4c719ee9f9cd857d"
700 | ]
701 | ]
702 | [
703 | "https://github.com/hannesm/duration/releases/download/v0.2.1/duration-0.2.1.tbz"
704 | "duration"
705 | [
706 | "sha256=c738c1f38cfb99820c121cd3ddf819de4b2228f0d50eacbd1cc3ce99e7c71e2b"
707 | "sha512=0de9e15c7d6188872ddd9994f08616c4a1822e4ac92724efa2c312fbb2fc44cd7cbe4b36bcf66a8451d510c1fc95de481760afbcacb8f83e183262595dcf5f0c"
708 | ]
709 | ]
710 | [
711 | "https://github.com/inhabitedtype/angstrom/archive/0.15.0.tar.gz"
712 | "angstrom"
713 | ["md5=5104768c404ea92fd0a53a5b0f75cd50"]
714 | ]
715 | [
716 | "https://github.com/inhabitedtype/bigstringaf/archive/0.9.1.tar.gz"
717 | "bigstringaf"
718 | ["md5=909fdc277cf03096a35b565325d5314a"]
719 | ]
720 | [
721 | "https://github.com/inhabitedtype/faraday/archive/0.8.2.tar.gz"
722 | "faraday"
723 | ["md5=307dccc4ea67ff0eeff394701d05bbe7"]
724 | ]
725 | [
726 | "https://github.com/janestreet/base/archive/refs/tags/v0.15.1.tar.gz"
727 | "base"
728 | [
729 | "sha256=755e303171ea267e3ba5af7aa8ea27537f3394d97c77d340b10f806d6ef61a14"
730 | ]
731 | ]
732 | [
733 | "https://github.com/janestreet/ocaml-compiler-libs/releases/download/v0.12.4/ocaml-compiler-libs-v0.12.4.tbz"
734 | "ocaml-compiler-libs"
735 | [
736 | "sha256=4ec9c9ec35cc45c18c7a143761154ef1d7663036a29297f80381f47981a07760"
737 | "sha512=978dba8dfa61f98fa24fda7a9c26c2e837081f37d1685fe636dc19cfc3278a940cf01a10293504b185c406706bc1008bc54313d50f023bcdea6d5ac6c0788b35"
738 | ]
739 | ]
740 | [
741 | "https://github.com/janestreet/result/releases/download/1.5/result-1.5.tbz"
742 | "result"
743 | ["md5=1b82dec78849680b49ae9a8a365b831b"]
744 | ]
745 | [
746 | "https://github.com/janestreet/sexplib0/archive/refs/tags/v0.15.1.tar.gz"
747 | "sexplib0"
748 | ["md5=ab8fd6273f35a792cad48cbb3024a7f9"]
749 | ]
750 | [
751 | "https://github.com/kit-ty-kate/bytes/archive/v0.1.0.tar.gz"
752 | "bytes"
753 | [
754 | "sha256=795b9bf545841714aaf0e517b62834a589937f65ad815ed4589ea56fa614d238"
755 | ]
756 | ]
757 | [
758 | "https://github.com/mirage/bigarray-compat/releases/download/v1.1.0/bigarray-compat-1.1.0.tbz"
759 | "bigarray-compat"
760 | [
761 | "sha256=434469a48d5c84e80d621b13d95eb067f8138c1650a1fd5ae6009a19b93718d5"
762 | "sha512=7be283fd957ee168ce1e62835d22114da405e4b7da9619b4f2030a832d45ca210a0c8f1d1c57c92e224f3512308a8a0f0923b94f44b6f582acbe0e7728d179d4"
763 | ]
764 | ]
765 | [
766 | "https://github.com/mirage/digestif/releases/download/v1.1.4/digestif-1.1.4.tbz"
767 | "digestif"
768 | [
769 | "sha256=c3793e720f0da8054f6286c545c821a7febe882e7f4e5497ec89b15a353511e1"
770 | "sha512=a4014f65a3be370761833fd98f3916d0a64ada6f99ac016890f5ae98ec75a941836a5a1e145ae36372aeb6b48c66a0ad9907a4318bfc8dc0c237840edba1aef4"
771 | ]
772 | ]
773 | [
774 | "https://github.com/mirage/eqaf/releases/download/v0.9/eqaf-0.9.tbz"
775 | "eqaf"
776 | [
777 | "sha256=ec0e28a946ac6817f95d5854f05a9961ae3a8408bb610e79cfad01b9b255dfe0"
778 | "sha512=4df7fd3ea35156953a172c1a021aab05b8b122ee8d3cfdb34f96edb1b5133d1fe2721b90cb64287841d770b16c2ffe70559c66e90f8d61a92b73857da22548c4"
779 | ]
780 | ]
781 | [
782 | "https://github.com/mirage/ke/releases/download/v0.6/ke-0.6.tbz"
783 | "ke"
784 | [
785 | "sha256=61217207e2200b04b17759736610ff9208269a647f854cb5ae72cdac0d672305"
786 | "sha512=be277780a7a6c9109068b6c8d54fa88c35180802ff86951516a32a6b7c0335fd6584753d1c670e02632b3956c09ae31bfec70e3dd5ea94697e9e032ba3b9248b"
787 | ]
788 | ]
789 | [
790 | "https://github.com/mirage/mirage-clock/releases/download/v4.2.0/mirage-clock-4.2.0.tbz"
791 | "mirage-clock"
792 | [
793 | "sha256=fa17d15d5be23c79ba741f5f7cb88ed7112de16a4410cea81c71b98086889847"
794 | "sha512=05a359dc8400d4ca200ff255dbd030acd33d2c4acb5020838f772c02cdb5f243f3dbafbc43a8cd51e6b5923a140f84c9e7ea25b2c0fa277bb68b996190d36e3b"
795 | ]
796 | ]
797 | [
798 | "https://github.com/mirage/mirage-crypto/releases/download/v0.11.1/mirage-crypto-0.11.1.tbz"
799 | "mirage-crypto"
800 | [
801 | "sha256=0cda147b20a92bf70c5c9eb7f55c675d03abc76bbd4b1f0dd9cf7fc38016d29c"
802 | "sha512=36e184c950f8ac51283cbcc2ccea84240c9369c3a5b36d8d0253d45a53b979ca97bd779450c79510205a9d257cc5916c42ab217111b4cad62758292648c79bc3"
803 | ]
804 | ]
805 | [
806 | "https://github.com/mirage/ocaml-base64/releases/download/v3.5.1/base64-3.5.1.tbz"
807 | "ocaml-base64"
808 | [
809 | "sha256=d8fedaa59bd12feae7acc08b5928dd478aac523f4ca8d240470d2500651c65ed"
810 | "sha512=278bd2029800d90ed88ff59b9de723013e645523556a1667b64178d6b5058a7d6da91efffef3589c35569b5fa10ddee74c93f5a3d156b9146c8af5b7fe44aeaf"
811 | ]
812 | ]
813 | [
814 | "https://github.com/mirage/ocaml-cstruct/releases/download/v6.2.0/cstruct-6.2.0.tbz"
815 | "ocaml-cstruct"
816 | [
817 | "sha256=9a78073392580e8349148fa3ab4b1b2e989dc9d30d07401b04c96b7c60f03e62"
818 | "sha512=8d33fe6b3707a3994d0225cd33cadde0bb2ca834ef01096e3df33a08e4a8c6d02ebccddf558a73988b8a5595b65fdc10de61efbf872c6c9e55c719c7e19c463d"
819 | ]
820 | ]
821 | [
822 | "https://github.com/mirage/ocaml-magic-mime/releases/download/v1.3.0/magic-mime-1.3.0.tbz"
823 | "ocaml-magic-mime"
824 | [
825 | "sha256=d835948a288d7efd9c49e1958b9f54260769ff4a0eb1f0829541876622f7cd9c"
826 | "sha512=15da02dcd044805401454cca25cf4b564ffcf1cd929f7422fd43c7911e3c20f6e456bd49be4c1916b706104a1f4acc5bb06c7e52bf6b8cc8895c519a0a0ac051"
827 | ]
828 | ]
829 | [
830 | "https://github.com/mirage/ocaml-uri/releases/download/v4.2.0/uri-v4.2.0.tbz"
831 | "ocaml-uri"
832 | [
833 | "sha256=c5c013d940dbb6731ea2ee75c2bf991d3435149c3f3659ec2e55476f5473f16b"
834 | "sha512=119e39bf53db9e94383a4e3a3df492b60b2db097266b3a8660de431ad85bc87997718305972fd2abbfb529973475ce6b210ba5e34d12e85a5dabbb0e24130aa1"
835 | ]
836 | ]
837 | [
838 | "https://github.com/mirage/pecu/releases/download/v0.6/pecu-v0.6.tbz"
839 | "pecu"
840 | [
841 | "sha256=a9d2b7da444c83b20f879f6c3b7fc911d08ac1e6245ad7105437504f9394e5c7"
842 | "sha512=8cae31da1fcb8b684a949846b1668131de244fbb89faf7421761da208f87092523a9e184e91a04c26739e6793501307b30ed255d540dcb268b171b7a56b56e24"
843 | ]
844 | ]
845 | [
846 | "https://github.com/mmottl/sqlite3-ocaml/releases/download/5.1.0/sqlite3-5.1.0.tbz"
847 | "sqlite3-ocaml"
848 | [
849 | "sha256=bb0db711691a8dfa24fe29ec4ecb6912444ad90e0f4c447af89831e6d1dffea5"
850 | "sha512=a5e3070f95ccfaffd51de7081d55a204a24d3d277a87fab985a0418e5dd1478ed0462ebaa4dbd4a8bbaf75edd1e216300601d033a7cf8ab2a8ed3b88bbcb9e64"
851 | ]
852 | ]
853 | [
854 | "https://github.com/ocaml-community/cppo/archive/v1.6.9.tar.gz"
855 | "cppo"
856 | [
857 | "md5=d23ffe85ac7dc8f0afd1ddf622770d09"
858 | "sha512=26ff5a7b7f38c460661974b23ca190f0feae3a99f1974e0fd12ccf08745bd7d91b7bc168c70a5385b837bfff9530e0e4e41cf269f23dd8cf16ca658008244b44"
859 | ]
860 | ]
861 | [
862 | "https://github.com/ocaml-community/yojson/releases/download/2.0.2/yojson-2.0.2.tbz"
863 | "yojson"
864 | [
865 | "sha256=876bb6f38af73a84a29438a3da35e4857c60a14556a606525b148c6fdbe5461b"
866 | "sha512=9e150689a814a64e53e361e336fe826df5a3e3851d1367fda4a001392175c29348de55db0b7d7ba18539dec2cf78198efcb7f41b77a9861763f5aa97c05509ad"
867 | ]
868 | ]
869 | [
870 | "https://github.com/ocaml-dune/csexp/releases/download/1.5.2/csexp-1.5.2.tbz"
871 | "csexp"
872 | [
873 | "sha256=1a14dd04bb4379a41990248550628c77913a9c07f3c35c1370b6960e697787ff"
874 | "sha512=be281018bcfc20d4db14894ef51c4b836d6338d2fdfe22e63d46f405f8dea7349e16f1c0ecd65f73d4c85a2a80e618cdbb8c9dafcbb9f229f04f1adca5b1973c"
875 | ]
876 | ]
877 | [
878 | "https://github.com/ocaml-ppx/ocaml-syntax-shims/releases/download/1.0.0/ocaml-syntax-shims-1.0.0.tbz"
879 | "ocaml-syntax-shims"
880 | [
881 | "sha256=89b2e193e90a0c168b6ec5ddf6fef09033681bdcb64e11913c97440a2722e8c8"
882 | "sha512=75c4c6b0bfa1267a8a49a82ba494d08cf0823fc8350863d6d3d4971528cb09e5a2a29e2981d04c75e76ad0f49360b05a432c9efeff9a4fbc1ec6b28960399852"
883 | ]
884 | ]
885 | [
886 | "https://github.com/ocaml-ppx/ppx_derivers/archive/1.2.1.tar.gz"
887 | "ppx_derivers"
888 | ["md5=5dc2bf130c1db3c731fe0fffc5648b41"]
889 | ]
890 | [
891 | "https://github.com/ocaml-ppx/ppx_deriving/releases/download/v5.2.1/ppx_deriving-v5.2.1.tbz"
892 | "ppx_deriving"
893 | [
894 | "sha256=e96b5fb25b7632570e4b329e22e097fcd4b8e8680d1e43ef003a8fbd742b0786"
895 | "sha512=f28cd778a2d48ababa53f73131b25229a11b03685610d020b7b9228b1e25570891cd927b37475aeda49be72debaf5f2dda4c1518a0965db7a361c0ebe325a8d2"
896 | ]
897 | ]
898 | [
899 | "https://github.com/ocaml-ppx/ppxlib/releases/download/0.29.1/ppxlib-0.29.1.tbz"
900 | "ppxlib"
901 | [
902 | "sha256=c8ea8c8770414fdba6612e7f2d814b21a493daa974ea862a90c8e6c766e5dd79"
903 | "sha512=edc468e9111cc26e31825e475fd72f55123a22fe86548e07e7d111796fecb8d60359b1b53c7eac383e5e2114cbae74dfd9c166f330e84cbeab4ddfd5797e322f"
904 | ]
905 | ]
906 | [
907 | "https://github.com/ocaml/camlp-streams/archive/v5.0.1.tar.gz"
908 | "camlp-streams"
909 | [
910 | "md5=afc874b25f7a1f13e8f5cfc1182b51a7"
911 | "sha512=2efa8dd4a636217c8d49bac1e4e7e5558fc2f45cfea66514140a59fd99dd08d61fb9f1e17804997ff648b71b13820a5d4a1eb70fed9d848aa2abd6e41f853c86"
912 | ]
913 | ]
914 | [
915 | "https://github.com/ocaml/dune/releases/download/3.7.1/dune-3.7.1.tbz"
916 | "dune_"
917 | [
918 | "sha256=adfc38f14c0188a2ad80d61451d011d27ab8839b717492d7ad42f7cb911c54c3"
919 | "sha512=a74cd77ac7714f9434b5991c6dc02c6e6a2f46071d993a8985a9c9f0105182bb9e310ae2bcf7cf1d411c848d1a665e0fc0111b3597e5b1c6b634c1d398bea432"
920 | ]
921 | ]
922 | [
923 | "https://github.com/ocaml/ocaml-re/releases/download/1.10.4/re-1.10.4.tbz"
924 | "ocaml-re"
925 | [
926 | "sha256=83eb3e4300aa9b1dc7820749010f4362ea83524742130524d78c20ce99ca747c"
927 | "sha512=92b05cf92c389fa8c753f2acca837b15dd05a4a2e8e2bec7a269d2e14c35b1a786d394258376648f80b4b99250ba1900cfe68230b8385aeac153149d9ce56099"
928 | ]
929 | ]
930 | [
931 | "https://github.com/ocaml/stdlib-shims/releases/download/0.3.0/stdlib-shims-0.3.0.tbz"
932 | "stdlib-shims"
933 | [
934 | "sha256=babf72d3917b86f707885f0c5528e36c63fccb698f4b46cf2bab5c7ccdd6d84a"
935 | "sha512=1151d7edc8923516e9a36995a3f8938d323aaade759ad349ed15d6d8501db61ffbe63277e97c4d86149cf371306ac23df0f581ec7e02611f58335126e1870980"
936 | ]
937 | ]
938 | [
939 | "git+https://github.com/ocsigen/lwt.git#cc05e2bda6c34126a3fd8d150ee7cddb3b8a440b"
940 | "lwt"
941 | ]
942 | [
943 | "https://github.com/ocsigen/lwt_ssl/releases/download/1.2.0/lwt_ssl-1.2.0.tbz"
944 | "lwt_ssl"
945 | [
946 | "sha256=b3020ad27aecf377e1c3f2740a08b36dcbba991f843066511357410548889a77"
947 | "sha512=cf2ef7d4db26e40c044e743ce85849a10eb57c916cbd7d6291bf4458291689098293bfb4cd7f1023f3ae8bc8e9a68cb2c7470669501a9b44695659405a75aa00"
948 | ]
949 | ]
950 | [
951 | "https://github.com/ocsigen/tyxml/releases/download/4.5.0/tyxml-4.5.0.tbz"
952 | "tyxml"
953 | [
954 | "sha256=c69accef5df4dd89d38f6aa0baad01e8fda4e9e98bb7dad61bec1452c5716068"
955 | "sha512=772535441b09c393d53c27152e65f404a0a541aa0cea1bda899a8d751ab64d1729237e583618c3ff33d75e3865d53503d1ea413c6bbc8c68c413347efd1709b3"
956 | ]
957 | ]
958 | [
959 | "https://github.com/paurkedal/ocaml-caqti/releases/download/v1.9.0/caqti-v1.9.0.tbz"
960 | "ocaml-caqti"
961 | [
962 | "sha256=e1f580848faf3a54f23174067f2c75f77f6a2fe50ca8bc923428d0e1841192c5"
963 | "sha512=7a11edfcfbbe4855347b066e222cf6bf46d1afedcd4978661b9a2b3931921faa1768a6bc24031fd3afa84537fe2adc8b139399deb77120461bee8fb394d68e82"
964 | ]
965 | ]
966 | [
967 | "https://github.com/pqwy/psq/releases/download/v0.2.1/psq-0.2.1.tbz"
968 | "psq"
969 | [
970 | "sha256=42005f533eabe74b1799ee32b8905654cd66a22bed4af2bd266b28d8462cd344"
971 | "sha512=8a8dfe20dc77e1cf38a7b1a7fc76f815c71a4ffe04627151b855feaba8f1ae742594739d1b7a45580b5b24a2cd99b58516f6b5c8d858aa314201f4a6422101ee"
972 | ]
973 | ]
974 | [
975 | "https://github.com/rgrinberg/stringext/releases/download/1.6.0/stringext-1.6.0.tbz"
976 | "stringext"
977 | [
978 | "sha256=db41f5d52e9eab17615f110b899dfeb27dd7e7f89cd35ae43827c5119db206ea"
979 | "sha512=d8ebe40f42b598a9bd99f1ef4b00ba93458385a4accd121af66a0bf3b3f8d7135f576740adf1a43081dd409977c2219fd4bdbb5b3d1308890d301d553ed49900"
980 | ]
981 | ]
982 | [
983 | "git+https://github.com/roddyyaga/pg_query-ocaml.git#9b37b2b2937fe9e1e52f778cd897acef458e8327"
984 | "pg_query-ocaml"
985 | ]
986 | [
987 | "https://github.com/roddyyaga/ppx_rapper/archive/3.1.0.tar.gz"
988 | "ppx_rapper"
989 | [
990 | "md5=d6ce377b4a0d8c1c8f9fefd643c35282"
991 | "sha512=16cdb7bc4632ebbd77cbed26c1852ad38274c7ae2dd7b3455c6671f3579137f5598fa044b80272457ef859cf79831b087c63de13252630d85b0b8ff4dcfd2125"
992 | ]
993 | ]
994 | [
995 | "https://github.com/savonet/ocaml-ssl/releases/download/0.5.13/ssl-0.5.13.tbz"
996 | "ocaml-ssl"
997 | [
998 | "sha256=d68550952c8fed5e7922b273597a4da801c254edd21a971360f510529e1c2b39"
999 | "sha512=0d6b4265bf75c3d9b7f262486ab52410f9a16208193171d7a5b74e4e233dfab847c153aa711aaf9114f4997546c4c39d45273ce20e9cbdaaf5d41c58c1635adc"
1000 | ]
1001 | ]
1002 | [
1003 | "https://github.com/tmattio/dream-livereload/releases/download/0.2.0/dream-livereload-0.2.0.tbz"
1004 | "dream-livereload"
1005 | [
1006 | "sha256=f9650347225b2e42b2e45419f4b98435c432c0a295b25fd7e1a7dc219024f8de"
1007 | "sha512=ab19e04bd6f941b769f75fc10114f3f1948cb55ed65e6f4ab8ddbf51987b18594cbb5ad4af24eeb3eb24f735e967d9e0a07112f5b7e53c2fc976cb3e5b5cdee7"
1008 | ]
1009 | ]
1010 | [
1011 | "https://github.com/yallop/ocaml-integers/archive/0.7.0.tar.gz"
1012 | "ocaml-integers"
1013 | ["md5=201cf24143d7cb9a3921d572b6e6c42c"]
1014 | ]
1015 | [
1016 | "https://gitlab.inria.fr/fpottier/menhir/-/archive/20230415/archive.tar.gz"
1017 | "menhir"
1018 | [
1019 | "md5=7c4b51e1b666711af04f7832ebc90618"
1020 | "sha512=aa8a34c173d9a82d3503919de8377f1b8c9ff721882486f0b5ae2bdb9b22ee7f5ba8f6ef25e00fbb35704fac9fc3bda71908512ed4cbd345d9dc29d6ede149b2"
1021 | ]
1022 | ]
1023 | [
1024 | "https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_here-v0.15.0.tar.gz"
1025 | "ppx_here"
1026 | [
1027 | "sha256=c5bc027c938a4893267c12e5ded6d7391b89851cf69094154ad9d824c3e0cadf"
1028 | ]
1029 | ]
1030 | [
1031 | "https://ocaml.janestreet.com/ocaml-core/v0.15/files/ppx_let-v0.15.0.tar.gz"
1032 | "ppx_let"
1033 | [
1034 | "sha256=64742c11eab6d6915a5213b20648af16ea2f65771170887ad91f8f1da38f3655"
1035 | ]
1036 | ]
1037 | ]
1038 | x-opam-monorepo-root-packages: ["ohtml" "ppx_model"]
1039 | x-opam-monorepo-version: "0.3"
1040 |
--------------------------------------------------------------------------------
/ppx-model/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (name pp)
3 | (modules pp)
4 | (libraries ppxlib)
5 | (preprocess (pps ppxlib.metaquot ppx_model)))
6 |
7 | (library
8 | (name ppx_model)
9 | (kind ppx_deriver)
10 | (modules model)
11 | (package ppx_model)
12 | (flags (:standard -w -27))
13 | (libraries ppxlib base fmt)
14 | (preprocess
15 | (pps ppxlib.metaquot)))
16 |
17 |
--------------------------------------------------------------------------------
/ppx-model/model.ml:
--------------------------------------------------------------------------------
1 | open Ppxlib
2 | open Ast_helper
3 | open Ast_builder.Default
4 | open Base
5 |
6 | let core_type_is_option ct =
7 | match ct.ptyp_desc with
8 | | Ptyp_constr ({ txt = Lident "option"; _ }, _) -> true
9 | | _ -> false
10 | ;;
11 |
12 | let label_to_output id_label label =
13 | match label.pld_name.txt with
14 | | "id" -> Fmt.str "@%s{id}" id_label
15 | | _ when core_type_is_option label.pld_type ->
16 | let ct =
17 | List.hd_exn
18 | (match label.pld_type.ptyp_desc with
19 | | Ptyp_constr (_, core_types) -> core_types
20 | | _ -> assert false)
21 | in
22 | Fmt.str "@%a?{%s}" Pprintast.core_type ct label.pld_name.txt
23 | | _ -> Fmt.str "@%a{%s}" Pprintast.core_type label.pld_type label.pld_name.txt
24 | ;;
25 |
26 | let label_to_input label =
27 | if core_type_is_option label.pld_type
28 | then (
29 | let ct =
30 | List.hd_exn
31 | (match label.pld_type.ptyp_desc with
32 | | Ptyp_constr (_, core_types) -> core_types
33 | | _ -> assert false)
34 | in
35 | Fmt.str "%%%a?{%s}" Pprintast.core_type ct label.pld_name.txt)
36 | else
37 | Caml.Format.asprintf "%%%a{%s}" Pprintast.core_type label.pld_type label.pld_name.txt
38 | ;;
39 |
40 | let make_str_const s loc = Exp.constant (Pconst_string (s, loc, None))
41 |
42 | let mk_select ~selections ~table_name ~id_label ~loc =
43 | let selection =
44 | Fmt.str {| SELECT %s FROM %s WHERE id = %%%s{id} |} selections table_name id_label
45 | in
46 | make_str_const selection loc
47 | ;;
48 |
49 | let mk_select_many ~selections ~table_name ~loc =
50 | let select_many = Fmt.str {| SELECT %s FROM %s |} selections table_name in
51 | make_str_const select_many loc
52 | ;;
53 |
54 | let mk_create mode ~id_lbl ~fields ~loc ~table_name =
55 | let label_to_output = label_to_output "id" in
56 | let create_select =
57 | List.map fields ~f:(fun l -> l.pld_name.txt) |> String.concat ~sep:","
58 | in
59 | let create_values = List.map fields ~f:label_to_input |> String.concat ~sep:"," in
60 | let create_results = List.map fields ~f:label_to_output |> String.concat ~sep:"," in
61 | let create =
62 | Fmt.str
63 | {| INSERT INTO %s ( %s ) VALUES ( %s ) RETURNING @%s{id},%s |}
64 | table_name
65 | create_select
66 | create_values
67 | id_lbl
68 | create_results
69 | in
70 | let create = make_str_const create loc in
71 | match mode with
72 | | `Args -> [%expr [%rapper get_one [%e create] record_out]]
73 | | `Record -> [%expr [%rapper get_one [%e create] record_in record_out]]
74 | ;;
75 |
76 | let mk_get_by_id ~selections ~table_name ~key ~foreign_name ~foreign_key ~loc =
77 | let get_query =
78 | Fmt.str
79 | {| SELECT %s FROM %s INNER JOIN %s ON %s.%s = %s.%s WHERE %s.%s = %%int{%s} |}
80 | selections
81 | table_name
82 | foreign_name
83 | table_name
84 | key
85 | foreign_name
86 | foreign_key
87 | table_name
88 | key
89 | key
90 | in
91 | make_str_const get_query loc
92 | ;;
93 |
94 | let mk_type_data ~loc ~labels =
95 | let type_data =
96 | type_declaration
97 | ~loc
98 | ~name:Location.{ txt = "data"; loc }
99 | ~params:[]
100 | ~cstrs:[]
101 | ~kind:(Ptype_record labels)
102 | ~manifest:None
103 | ~private_:Public
104 | in
105 | pstr_type ~loc Nonrecursive [ type_data ]
106 | ;;
107 |
108 | (* let extract_payload () = *)
109 | (* let open Ast_pattern in *)
110 | (* (* (pstr_value nonrecursive (value_binding ~pat:(pstring __) ~expr:__ ^:: nil) *) *)
111 | (* pstr *)
112 | (* (pstr_value nonrecursive (value_binding ~pat:(pstring __) ~expr:__ ^:: nil) *)
113 | (* ^:: nil) *)
114 | (* ;; *)
115 |
116 | let payloader () =
117 | let open Ast_pattern in
118 | (* let x = attribute ~name:("foreign", __, __) ~payload:__ in *)
119 | let x = single_expr_payload (estring __) in
120 | x
121 | ;;
122 |
123 | let mk_foreign ~loc ~labels ~selections ~table_name =
124 | let foreign =
125 | List.find labels ~f:(fun l ->
126 | List.find l.pld_attributes ~f:(fun a -> String.(a.attr_name.txt = "foreign"))
127 | |> Option.is_some)
128 | in
129 | let foreign =
130 | Option.map foreign ~f:(fun foreign ->
131 | let payload = (List.hd_exn foreign.pld_attributes).attr_payload in
132 | let _ = Ast_pattern.parse_res (payloader ()) loc payload (fun a -> a) in
133 | let foreign_key = foreign.pld_name.txt in
134 | let foreign_name =
135 | match payload with
136 | | PStr
137 | [ { pstr_desc =
138 | Pstr_eval ({ pexp_desc = Pexp_ident { txt = Lident lident; _ }; _ }, _)
139 | ; _
140 | }
141 | ] -> lident
142 | | _ -> Location.raise_errorf ~loc "Unsupported payload for @foreign"
143 | in
144 | let foreign_query =
145 | mk_get_by_id
146 | ~loc
147 | ~selections
148 | ~table_name
149 | ~key:foreign_key
150 | ~foreign_name
151 | ~foreign_key
152 | in
153 | let body = [%expr [%rapper get_many [%e foreign_query] record_out]] in
154 | let pat = ppat_var ~loc { txt = "get_by_" ^ foreign_key; loc } in
155 | let binding = Ast_builder.Default.value_binding ~loc ~pat ~expr:body in
156 | pstr_value ~loc Nonrecursive [ binding ])
157 | in
158 | match foreign with
159 | | Some foreign -> foreign
160 | | None -> [%stri let _ = 0]
161 | ;;
162 |
163 | let generate_impl ~ctxt (_rec_flag, type_decls) table_name =
164 | let loc = Expansion_context.Deriver.derived_item_loc ctxt in
165 | List.map type_decls ~f:(function
166 | | { ptype_kind = Ptype_record labels; ptype_manifest; ptype_name; _ } ->
167 | let table_name = Option.value_exn table_name in
168 | let id_lbl = List.find_exn labels ~f:(fun l -> String.(l.pld_name.txt = "id")) in
169 | let id_lbl =
170 | match id_lbl.pld_type.ptyp_desc with
171 | | Ptyp_constr ({ txt = Ldot (Lident li, _); _ }, _) -> li
172 | | _ -> assert false
173 | in
174 | let fields = List.filter labels ~f:(fun l -> String.(l.pld_name.txt <> "id")) in
175 | let label_to_output = label_to_output id_lbl in
176 | let selections = List.map labels ~f:label_to_output |> String.concat ~sep:"," in
177 | let foreign = mk_foreign ~loc ~labels ~selections ~table_name in
178 | let select = mk_select ~selections ~table_name ~id_label:id_lbl ~loc in
179 | let select_many = mk_select_many ~selections ~table_name ~loc in
180 | let create = mk_create `Args ~id_lbl ~fields ~table_name ~loc in
181 | let create_record = mk_create `Record ~id_lbl ~fields ~table_name ~loc in
182 | let type_data = mk_type_data ~loc ~labels:fields in
183 | [%stri
184 | include struct
185 | [%%i type_data]
186 |
187 | let create = [%e create]
188 | let create_record = [%e create_record]
189 | let read = [%rapper get_opt [%e select] record_out]
190 | let read_all = [%rapper get_many [%e select_many] record_out]
191 |
192 | [%%i foreign]
193 | end]
194 | | _ -> Location.raise_errorf ~loc "Cannot derive anything for this type")
195 | ;;
196 |
197 | let args =
198 | let open Deriving.Args in
199 | empty +> arg "table_name" (estring __)
200 | ;;
201 |
202 | let impl_generator = Deriving.Generator.V2.make args generate_impl
203 | let model = Deriving.add "model" ~str_type_decl:impl_generator
204 |
--------------------------------------------------------------------------------
/ppx-model/pp.ml:
--------------------------------------------------------------------------------
1 | let () = Ppxlib.Driver.standalone ()
2 |
--------------------------------------------------------------------------------
/ppx_model.opam:
--------------------------------------------------------------------------------
1 | # This file is generated by dune, edit dune-project instead
2 | opam-version: "2.0"
3 | maintainer: ["TJ DeVries"]
4 | authors: ["TJ DeVries"]
5 | license: "MIT"
6 | homepage: "https://github.com/tjdevries/OhtML"
7 | bug-reports: "https://github.com/tjdevries/OhtML/issues"
8 | depends: [
9 | "dune" {>= "3.7"}
10 | "ocaml"
11 | "ppxlib"
12 | "odoc" {with-doc}
13 | ]
14 | build: [
15 | ["dune" "subst"] {dev}
16 | [
17 | "dune"
18 | "build"
19 | "-p"
20 | name
21 | "-j"
22 | jobs
23 | "@install"
24 | "@runtest" {with-test}
25 | "@doc" {with-doc}
26 | ]
27 | ]
28 | dev-repo: "git+https://github.com/tjdevries/OhtML.git"
29 |
--------------------------------------------------------------------------------
/principles.md:
--------------------------------------------------------------------------------
1 | IS:
2 | - See the code
3 | - HATEOAS
4 | - "no runtime errors"
5 | - use type system
6 | - template type safety
7 |
8 | IS NOT:
9 | - lots of interactivity (google sheets)
10 |
--------------------------------------------------------------------------------
/scratch/aria.md:
--------------------------------------------------------------------------------
1 |
2 | should probably go read about that at some point. think we can probably make a bunch of it automatically show up via different functions
3 |
--------------------------------------------------------------------------------
/scratch/example.ml:
--------------------------------------------------------------------------------
1 | module type ROUTE = sig
2 | val name : string
3 | end
4 |
5 | module Route (T : ROUTE) = struct
6 | include T
7 |
8 | let post = T.name
9 | end
10 |
11 | module Increment = Route (struct
12 | let name = "increment"
13 | end)
14 |
15 | let x = Increment.post
16 | let y = Increment.name
17 |
--------------------------------------------------------------------------------
/scratch/rsjs.md:
--------------------------------------------------------------------------------
1 |
2 | ```html
3 |
7 | ```
8 |
9 | 1. Invoke a JavaScript behavior with a data attribute.
10 | 2. Mark relevant descendant elements.
11 |
12 | ```javascript
13 | // counter.js (1)
14 | document.querySelectorAll("[data-counter]") // (2)
15 | .forEach(el => {
16 | const
17 | output = el.querySelector("[data-counter-output]"),
18 | increment = el.querySelector("[data-counter-increment]"); // (3)
19 |
20 | increment.addEventListener("click", e => output.textContent++); // (4)
21 | });
22 | ```
23 | 1. File should have the same name as the data attribute, so that we can locate it easily.
24 | 2. Get all elements that invoke this behavior.
25 | 3. Get any child elements we need.
26 | 4. Register event handlers.
27 |
--------------------------------------------------------------------------------
/static/home.css:
--------------------------------------------------------------------------------
1 | :root {
2 | background: black;
3 | color: orange;
4 | }
5 |
6 | #name {
7 | background-color: red;
8 | }
9 |
10 | #password {
11 | background-color: green;
12 | }
13 |
14 | #twitchchat {
15 | display: flex;
16 | flex-direction: column;
17 | gap: 1rem;
18 | align-items: center;
19 | }
20 |
21 | label {
22 | width: 200px;
23 | }
24 |
25 | input {
26 | padding: 0 5px;
27 | }
28 |
29 | @keyframes fade-in {
30 | from {
31 | opacity: 0;
32 | }
33 | }
34 |
35 | @keyframes fade-out {
36 | to {
37 | opacity: 0;
38 | }
39 | }
40 |
41 | @keyframes slide-from-right {
42 | from {
43 | transform: translateX(90px);
44 | }
45 | }
46 |
47 | @keyframes slide-to-left {
48 | to {
49 | transform: translateX(-90px);
50 | }
51 | }
52 |
53 | /* define animations for the old and new content */
54 | ::view-transition-old(slide-it) {
55 | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
56 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
57 | }
58 | ::view-transition-new(slide-it) {
59 | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
60 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
61 | }
62 |
63 | /* tie the view transition to a given CSS class */
64 | .sample-transition {
65 | view-transition-name: slide-it;
66 | }
67 |
--------------------------------------------------------------------------------
/tables/001-dream_session.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE dream_session (
2 | id TEXT PRIMARY KEY,
3 | label TEXT NOT NULL,
4 | expires_at REAL NOT NULL,
5 | payload TEXT NOT NULL
6 | )
7 |
--------------------------------------------------------------------------------
/tables/010-image.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE images (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | data BLOB
4 | )
5 |
--------------------------------------------------------------------------------
/tables/010-user.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE users (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | password TEXT NOT NULL,
4 | name TEXT NOT NULL
5 | )
6 |
--------------------------------------------------------------------------------
/tables/020-exhibits.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE exhibits (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | user_id INTEGER NOT NULL,
4 | image_id INTEGER,
5 | content TEXT NOT NULL,
6 |
7 | FOREIGN KEY (user_id) REFERENCES users(id),
8 | FOREIGN KEY (image_id) REFERENCES images(id)
9 | )
10 |
--------------------------------------------------------------------------------
/tables/030-comments.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE comments (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | content TEXT NOT NULL,
4 | -- Add a foreign key to exhibits
5 | exhibit_id INTEGER NOT NULL,
6 |
7 | FOREIGN KEY (exhibit_id) REFERENCES exhibits(id)
8 | )
9 |
--------------------------------------------------------------------------------
/test/ppx/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (name pp)
3 | (modules pp)
4 | (libraries ppx_rapper ppx_model ppxlib))
5 |
6 | (rule
7 | (targets test.actual.ml)
8 | (deps (:pp pp.exe) (:input test.ml))
9 | (action (run ./%{pp} --impl %{input} -o %{targets})))
10 |
11 | (rule
12 | (alias runtest)
13 | (deps (file test.expected.ml) (file test.actual.ml))
14 | (action (diff test.expected.ml test.actual.ml)))
15 |
--------------------------------------------------------------------------------
/test/ppx/pp.ml:
--------------------------------------------------------------------------------
1 | let () = Ppxlib.Driver.standalone ()
2 |
--------------------------------------------------------------------------------
/test/ppx/test.expected.ml:
--------------------------------------------------------------------------------
1 | module User =
2 | struct
3 | type t = {
4 | id: int ;
5 | name: string }[@@deriving model]
6 | include struct let _ = fun (_ : t) -> ()
7 | let read _ = 10
8 | let _ = read end[@@ocaml.doc "@inline"][@@merlin.hide ]
9 | end
10 |
--------------------------------------------------------------------------------
/test/ppx/test.ml:
--------------------------------------------------------------------------------
1 | module User = struct
2 | type t =
3 | { id : int
4 | ; name : string
5 | }
6 | [@@deriving model]
7 | end
8 |
--------------------------------------------------------------------------------