summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-06-23 06:02:52 +0700
committerpolwex <polwex@sortug.com>2025-06-23 06:02:52 +0700
commitf4459658a0cad4b7615c01af9c3f87fb4d0233e0 (patch)
treea5f2403e420154abd95dffdc6c45045c8530599a
parentd653f488017b1904fb0089d2bf308ae042240f38 (diff)
working working
-rw-r--r--CLAUDE.md25
-rw-r--r--NOTES.md23
-rw-r--r--bin/mainlite.ml2
-rw-r--r--lib/dune9
-rw-r--r--litedb/handler.ml159
-rw-r--r--litedb/query.ml45
-rw-r--r--litedb/router.ml73
-rw-r--r--profile.dump0
8 files changed, 327 insertions, 9 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b849fa7
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,25 @@
+# OCaml Routes Library DSL
+
+## The `@-->` Operator
+
+The `@-->` operator is from the `routes` library and is used to bind route patterns to handler functions.
+
+### Example:
+```ocaml
+`GET, (s "posts" /? nil) @--> Handler.get_posts
+```
+
+### Breaking it down:
+- `s "posts"` - matches the string "posts" in the URL path
+- `/?` - path concatenation operator
+- `nil` - end of path (no more segments)
+- `@-->` - "maps to" operator that binds the route to the handler
+
+So `(s "posts" /? nil) @--> Handler.get_posts` means "the route `/posts` maps to the `Handler.get_posts` function".
+
+### Other common operators in the routes library:
+- `/:` for path parameters (e.g., `s "user" /: int /? nil` matches `/user/123`)
+- `//` for wildcard paths
+- `<$>` for transforming matched values
+
+It's a DSL (domain-specific language) for expressing routes in a concise, type-safe way. \ No newline at end of file
diff --git a/NOTES.md b/NOTES.md
new file mode 100644
index 0000000..0fd67b5
--- /dev/null
+++ b/NOTES.md
@@ -0,0 +1,23 @@
+apparently janestreet style is to use double semicolons
+
+# Cline's Notes
+
+## Objective: Fix the `litedb` build
+
+The user is experiencing a build failure with the `litedb` library. The error message "The module Litedb.Router is an alias for module Litedb__Router, which is missing" indicates a problem with how `dune` is resolving the modules within the library.
+
+### What I've tried:
+
+1. **Adding `(modules ...)` to `litedb/dune`:** This was incorrect, as the `lib` directory works without it.
+2. **Creating `litedb/litedb.ml`:** This was also incorrect, as the `lib` directory doesn't have a main module file.
+3. **Modifying `bin/mainlite.ml`:** I've tried various ways of referencing the `Litedb` modules, but the error persists.
+
+### What I've learned:
+
+* The `lib` and `litedb` directories have nearly identical `dune` files.
+* The `lib` directory works correctly without a main module file or an explicit `(modules ...)` stanza.
+* The error is specific to the `litedb` library.
+
+### Next Steps:
+
+I need to re-evaluate my understanding of how `dune` resolves modules. I will now try to run the application again, but this time I will pay close attention to the build output to see if there are any other clues that I've missed.
diff --git a/bin/mainlite.ml b/bin/mainlite.ml
index 3fd7e0f..42b0009 100644
--- a/bin/mainlite.ml
+++ b/bin/mainlite.ml
@@ -9,7 +9,7 @@ let setup_log ?style_renderer level =
;;
let request_handler ~db_pool Server.{ request; _ } =
- match Combattant.Router.match_route request.meth request.target with
+ match Litedb.Router.match_route request.meth request.target with
| Some handler -> Result.get_ok @@ handler db_pool request
| None ->
Logs.info (fun d -> d "Não encontrei %S\n" request.target);
diff --git a/lib/dune b/lib/dune
index b5507d7..7f404b2 100644
--- a/lib/dune
+++ b/lib/dune
@@ -1,5 +1,12 @@
(library
(name combattant)
- (libraries piaf routes yojson caqti caqti-driver-postgresql caqti-eio ppx_rapper_eio)
+ (libraries
+ piaf
+ routes
+ yojson
+ caqti
+ caqti-driver-postgresql
+ caqti-eio
+ ppx_rapper_eio)
(preprocess
(pps ppx_rapper)))
diff --git a/litedb/handler.ml b/litedb/handler.ml
new file mode 100644
index 0000000..dd9251f
--- /dev/null
+++ b/litedb/handler.ml
@@ -0,0 +1,159 @@
+open Piaf
+
+type pool = ((module Rapper_helper.CONNECTION), Caqti_error.t) Caqti_eio.Pool.t
+
+(* This is the main handler function for the GET /posts endpoint. *)
+(* It takes a database connection pool and a Piaf request as input. *)
+let get_posts (db_pool : pool) (_request : Request.t) =
+ (* Use a connection from the pool. Caqti_eio.Pool.use handles acquiring and releasing the connection. *)
+ Caqti_eio.Pool.use
+ (fun conn ->
+ (* Call the get_poasts function from the Query module to fetch posts from the database. *)
+ let posts_or_error = Query.get_poasts () conn in
+ (* Pattern match on the result of the database query. *)
+ match posts_or_error with
+ (* If the query is successful, the result is a list of posts. *)
+ | Ok posts ->
+ (* Map the list of post tuples to a Yojson list. *)
+ let json =
+ `List
+ ((* For each post tuple, create a JSON object. *)
+ List.map
+ (fun (post : Query.post_summary) -> `Assoc [ "title", `String post.title ])
+ posts)
+ in
+ (* Return a 200 OK response with the JSON body. *)
+ Ok (Response.of_string ~body:(Yojson.Safe.to_string json) `OK)
+ (* If the query fails, log the error and return a 500 Internal Server Error response. *)
+ | Error err ->
+ Logs.err (fun m -> m "Database error: %a" Caqti_error.pp err);
+ Ok (Response.create `Internal_server_error))
+ db_pool
+;;
+
+(* Handler for GET /posts/:id - gets a single post by ID *)
+let get_post (db_pool : pool) post_id (_request : Request.t) =
+ Caqti_eio.Pool.use
+ (fun conn ->
+ let post_or_error = Query.get_poast post_id conn in
+ match post_or_error with
+ | Ok (Some post) ->
+ let json =
+ `Assoc [
+ "id", `Int post.id;
+ "title", `String post.title;
+ "content", `String post.content;
+ "date", `String post.date;
+ "tags", `String post.tags;
+ "url", `String post.url
+ ]
+ in
+ Ok (Response.of_string ~body:(Yojson.Safe.to_string json) `OK)
+ | Ok None ->
+ Ok (Response.create `Not_found)
+ | Error err ->
+ Logs.err (fun m -> m "Database error: %a" Caqti_error.pp err);
+ Ok (Response.create `Internal_server_error))
+ db_pool
+;;
+
+(* Handler for GET /comments/:id - gets a single comment by ID *)
+let get_comment (db_pool : pool) comment_id (_request : Request.t) =
+ Caqti_eio.Pool.use
+ (fun conn ->
+ let comment_or_error = Query.Query.comment ~id:comment_id conn in
+ match comment_or_error with
+ | Ok (Some comment) ->
+ let json =
+ `Assoc [
+ "id", `Int comment.id;
+ "content", `String comment.content;
+ "date", `String comment.date;
+ "tags", `String comment.tags;
+ "url", `String comment.url
+ ]
+ in
+ Ok (Response.of_string ~body:(Yojson.Safe.to_string json) `OK)
+ | Ok None ->
+ Ok (Response.create `Not_found)
+ | Error err ->
+ Logs.err (fun m -> m "Database error: %a" Caqti_error.pp err);
+ Ok (Response.create `Internal_server_error))
+ db_pool
+;;
+
+(* Handler for GET /users/:username/comments - gets comments by a user *)
+let get_user_comments (db_pool : pool) username (_request : Request.t) =
+ Caqti_eio.Pool.use
+ (fun conn ->
+ let comments_or_error = Query.Query.user_comments ~username conn in
+ match comments_or_error with
+ | Ok (Some comment) ->
+ let json =
+ `Assoc [
+ "id", `Int comment.id;
+ "content", `String comment.content;
+ "date", `String comment.date;
+ "tags", `String comment.tags;
+ "url", `String comment.url
+ ]
+ in
+ Ok (Response.of_string ~body:(Yojson.Safe.to_string json) `OK)
+ | Ok None ->
+ (* Return empty array if no comments found *)
+ Ok (Response.of_string ~body:"[]" `OK)
+ | Error err ->
+ Logs.err (fun m -> m "Database error: %a" Caqti_error.pp err);
+ Ok (Response.create `Internal_server_error))
+ db_pool
+;;
+
+(* Handler for GET /posts/:id/comments - gets comments for a post *)
+let get_post_comments (db_pool : pool) post_id (_request : Request.t) =
+ Caqti_eio.Pool.use
+ (fun conn ->
+ let comments_or_error = Query.Query.post_comments ~post_id conn in
+ match comments_or_error with
+ | Ok (Some comment) ->
+ let json =
+ `Assoc [
+ "id", `Int comment.id;
+ "content", `String comment.content;
+ "date", `String comment.date;
+ "tags", `String comment.tags;
+ "url", `String comment.url
+ ]
+ in
+ Ok (Response.of_string ~body:(Yojson.Safe.to_string json) `OK)
+ | Ok None ->
+ Ok (Response.of_string ~body:"[]" `OK)
+ | Error err ->
+ Logs.err (fun m -> m "Database error: %a" Caqti_error.pp err);
+ Ok (Response.create `Internal_server_error))
+ db_pool
+;;
+
+(* Handler for GET /comments/:id/children - gets child comments *)
+let get_comment_children (db_pool : pool) parent_id (_request : Request.t) =
+ Caqti_eio.Pool.use
+ (fun conn ->
+ let comments_or_error = Query.Query.comment_children ~post_id:parent_id conn in
+ match comments_or_error with
+ | Ok (Some comment) ->
+ let json =
+ `Assoc [
+ "id", `Int comment.id;
+ "content", `String comment.content;
+ "date", `String comment.date;
+ "tags", `String comment.tags;
+ "url", `String comment.url
+ ]
+ in
+ Ok (Response.of_string ~body:(Yojson.Safe.to_string json) `OK)
+ | Ok None ->
+ Ok (Response.of_string ~body:"[]" `OK)
+ | Error err ->
+ Logs.err (fun m -> m "Database error: %a" Caqti_error.pp err);
+ Ok (Response.create `Internal_server_error))
+ db_pool
+;;
diff --git a/litedb/query.ml b/litedb/query.ml
index 4929fdf..536347c 100644
--- a/litedb/query.ml
+++ b/litedb/query.ml
@@ -1,3 +1,28 @@
+(* Define record types for the query outputs *)
+type post_summary = {
+ id: int;
+ title: string;
+ content: string;
+ date: string;
+}
+
+type post = {
+ id: int;
+ title: string;
+ content: string;
+ date: string;
+ tags: string;
+ url: string;
+}
+
+type comment = {
+ id: int;
+ content: string;
+ date: string;
+ tags: string;
+ url: string;
+}
+
module Query = struct
let poasts =
[%rapper
@@ -6,7 +31,8 @@ module Query = struct
SELECT @int{id}, @string{title}, @string{content}, @string{date}
FROM Posts
ORDER BY id DESC LIMIT 100
- |sql}]
+ |sql}
+ record_out]
;;
let poast =
@@ -16,7 +42,8 @@ module Query = struct
SELECT @int{id}, @string{title}, @string{content}, @string{date}, @string{tags}, @string{url}
FROM Posts
WHERE id = %int{post_id}
- |sql}]
+ |sql}
+ record_out]
;;
let comment =
@@ -26,7 +53,8 @@ module Query = struct
SELECT @int{id}, @string{content}, @string{date}, @string{tags}, @string{url}
FROM Comments
WHERE id = %int{id}
- |sql}]
+ |sql}
+ record_out]
;;
let user_comments =
@@ -36,7 +64,8 @@ module Query = struct
SELECT @int{id}, @string{content}, @string{date}, @string{tags}, @string{url}
FROM Comments
WHERE author = %string{username}
- |sql}]
+ |sql}
+ record_out]
;;
let post_comments =
@@ -46,7 +75,8 @@ module Query = struct
SELECT @int{id}, @string{content}, @string{date}, @string{tags}, @string{url}
FROM Comments
WHERE post_id = %int{post_id}
- |sql}]
+ |sql}
+ record_out]
;;
let comment_children =
@@ -56,11 +86,12 @@ module Query = struct
SELECT @int{id}, @string{content}, @string{date}, @string{tags}, @string{url}
FROM Comments
WHERE parent= %int{post_id}
- |sql}]
+ |sql}
+ record_out]
;;
end
-let get_poasts conn = Query.poast conn
+let get_poasts conn = Query.poasts conn
let get_poast post_id conn = Query.poast ~post_id conn
(* db.exec("PRAGMA journal_mode = WAL"); *)
diff --git a/litedb/router.ml b/litedb/router.ml
new file mode 100644
index 0000000..ef9d5c5
--- /dev/null
+++ b/litedb/router.ml
@@ -0,0 +1,73 @@
+(* Import the StdLabels module which provides labeled versions of standard library functions *)
+open StdLabels
+(* Import the Routes library for type-safe HTTP routing *)
+open Routes
+(* Import Piaf for HTTP types like Method.t *)
+open Piaf
+
+(* Create a custom Map module keyed by HTTP methods (GET, POST, etc.) *)
+(* This allows us to organize routes by their HTTP verb *)
+module R = Map.Make (struct
+ (* The key type is Piaf's Method.t (represents HTTP methods) *)
+ type t = Method.t
+
+ (* Define how to compare two HTTP methods for ordering in the map *)
+ let compare a b =
+ (* Convert methods to strings (e.g., "GET", "POST") *)
+ let a_str = Method.to_string a in
+ let b_str = Method.to_string b in
+ (* Use standard string comparison *)
+ String.compare a_str b_str
+ ;;
+ end)
+
+(* Define all routes in the application *)
+let routes =
+ (* Use fold_left to build up a map of routes *)
+ List.fold_left
+ (* For each (verb, route) pair, add the route to the map under that verb *)
+ ~f:(fun acc (v, r) -> R.add_to_list v r acc)
+ (* Start with an empty map *)
+ ~init:R.empty
+ (* List of (HTTP method, route pattern -> handler) tuples *)
+ [ `GET, (s "posts" /? nil) @--> Handler.get_posts
+ (* `GET - HTTP GET method *)
+ (* s "posts" - matches the literal string "posts" *)
+ (* /? - path concatenation operator *)
+ (* nil - end of path (no more segments) *)
+ (* @--> - binds the route pattern to the handler function *)
+ (* Handler.get_posts - the function that handles this route *)
+ ; `GET, (s "posts" / int /? nil) @--> Handler.get_post
+ (* / int - captures an integer parameter (post ID) *)
+ ; `GET, (s "comments" / int /? nil) @--> Handler.get_comment
+ (* Get a single comment by ID *)
+ ; `GET, (s "users" / str / s "comments" /? nil) @--> Handler.get_user_comments
+ (* / str - captures a string parameter (username) *)
+ ; `GET, (s "posts" / int / s "comments" /? nil) @--> Handler.get_post_comments
+ (* Get all comments for a specific post *)
+ ; `GET, (s "comments" / int / s "children" /? nil) @--> Handler.get_comment_children
+ (* Get child comments (replies) for a comment *)
+ ]
+;;
+
+(* Transform the routes map by applying 'one_of' to each list of routes *)
+(* 'one_of' combines multiple routes into a single router that tries each in order *)
+let router = R.map one_of routes
+
+(* Function to match an incoming request against our routes *)
+let match_route verb path =
+ (* Try to find routes for the given HTTP verb in our map *)
+ match R.find_opt verb router with
+ (* If we have routes for this verb, try to match the path *)
+ | Some router ->
+ (* Use the Routes library's match' function to find a matching route *)
+ (match match' router ~target:path with
+ (* Exact match - the path matches completely *)
+ | FullMatch r -> Some r
+ (* Match with trailing slash - e.g., "/posts" matches "/posts/" *)
+ | MatchWithTrailingSlash r -> Some r
+ (* No routes matched the path *)
+ | NoMatch -> None)
+ (* We don't have any routes for this HTTP verb *)
+ | None -> None
+;;
diff --git a/profile.dump b/profile.dump
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/profile.dump