├── .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 |
4 | 0 5 | 6 |
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 | --------------------------------------------------------------------------------