From 7bcc90dc11d9ff0821e3d080e950d979e4d7469b Mon Sep 17 00:00:00 2001 From: Sambit Sahoo Date: Sat, 2 Nov 2024 21:14:52 +0530 Subject: [PATCH] feat: improve routing ergonomics by narrowing path --- .gitignore | 3 ++ .lasso-marks-tracker | 0 src/app/config.gleam | 18 +++++-- src/app/controllers/sessions.gleam | 21 ++++---- src/app/controllers/users.gleam | 20 +++++++ src/app/db/connection.gleam | 4 +- src/app/db/models/user.gleam | 6 +-- src/app/hooks/auth.gleam | 10 ++-- src/app/router.gleam | 7 ++- .../{base.gleam => base_serializer.gleam} | 0 .../{user.gleam => user_serializer.gleam} | 4 +- test/controllers/home.gleam | 40 ++++++++++++++ test/controllers/sessions.gleam | 29 +++++++++++ test/helpers/context.gleam | 9 ++++ test/okane_test.gleam | 52 +------------------ 15 files changed, 148 insertions(+), 75 deletions(-) delete mode 100644 .lasso-marks-tracker create mode 100644 src/app/controllers/users.gleam rename src/app/serializers/{base.gleam => base_serializer.gleam} (100%) rename src/app/serializers/{user.gleam => user_serializer.gleam} (88%) create mode 100644 test/controllers/home.gleam create mode 100644 test/controllers/sessions.gleam create mode 100644 test/helpers/context.gleam diff --git a/.gitignore b/.gitignore index af60594..68c6351 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ erl_crash.dump *.sqlite *.sqlite-* + + +.lasso-marks-tracker diff --git a/.lasso-marks-tracker b/.lasso-marks-tracker deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/config.gleam b/src/app/config.gleam index 0eefb5c..ac11e4a 100644 --- a/src/app/config.gleam +++ b/src/app/config.gleam @@ -5,15 +5,25 @@ import sqlight /// this is app context. when user is authenticated, it has user /// or db connection only pub type Context { - Context(db: sqlight.Connection, user: Option(user.User)) + Context( + db: sqlight.Connection, + user: Option(user.User), + scoped_segments: List(String), + ) } pub fn acquire_context(db: sqlight.Connection, run: fn(Context) -> a) -> a { - run(Context(db, None)) + run(Context(db, None, [])) } pub fn set_user(ctx: Context, user: user.User) { - let Context(db, ..) = ctx + let Context(db, _, scope) = ctx - Context(db, Some(user)) + Context(db, Some(user), scope) +} + +pub fn scope_to(ctx: Context, scoped_segments: List(String)) { + let Context(db, user, ..) = ctx + + Context(db, user, scoped_segments) } diff --git a/src/app/controllers/sessions.gleam b/src/app/controllers/sessions.gleam index 8e21aa6..d8b57ed 100644 --- a/src/app/controllers/sessions.gleam +++ b/src/app/controllers/sessions.gleam @@ -1,11 +1,12 @@ import app/config import app/db/models/user import app/hooks/auth -import app/serializers/base -import app/serializers/user as user_serializer +import app/serializers/base_serializer +import app/serializers/user_serializer import gleam/http import gleam/list import gleam/result +import gleam/string import wisp.{type Request, type Response} type LoginParam { @@ -38,7 +39,7 @@ fn handle_login(req: Request, ctx: config.Context) -> Response { case user_result { Error(_) -> { wisp.bad_request() - |> wisp.json_body(base.serialize_error( + |> wisp.json_body(base_serializer.serialize_error( "Either email or password is incorrect!", )) } @@ -60,7 +61,7 @@ fn handle_login(req: Request, ctx: config.Context) -> Response { False -> { wisp.bad_request() - |> wisp.json_body(base.serialize_error( + |> wisp.json_body(base_serializer.serialize_error( "Either email or password is incorrect !", )) } @@ -95,11 +96,10 @@ fn handle_register(req: Request, ctx: config.Context) { case my_user { Error(e) -> { - // TODO: add error specific response messages and codes - wisp.log_error(e.message) + wisp.log_error("DB: " <> string.inspect(e)) wisp.internal_server_error() - |> wisp.json_body(base.serialize_error("Error signing up!")) + |> wisp.json_body(base_serializer.serialize_error("Error signing up!")) } Ok(new_user) -> { @@ -109,9 +109,10 @@ fn handle_register(req: Request, ctx: config.Context) { } pub fn controller(req: Request, ctx: config.Context) -> Response { - case wisp.path_segments(req), req.method { - ["sessions", "login"], http.Post -> handle_login(req, ctx) - ["sessions", "register"], http.Post -> handle_register(req, ctx) + case ctx.scoped_segments, req.method { + ["login"], http.Post -> handle_login(req, ctx) + ["register"], http.Post -> handle_register(req, ctx) + _, _ -> wisp.not_found() } } diff --git a/src/app/controllers/users.gleam b/src/app/controllers/users.gleam new file mode 100644 index 0000000..f2d41b1 --- /dev/null +++ b/src/app/controllers/users.gleam @@ -0,0 +1,20 @@ +import app/config +import app/serializers/user_serializer +import gleam/http +import gleam/option +import wisp + +fn handle_current_user(_req: wisp.Request, ctx: config.Context) { + case ctx.user { + option.Some(user) -> wisp.ok() |> wisp.json_body(user_serializer.run(user)) + option.None -> wisp.not_found() + } +} + +pub fn controller(req: wisp.Request, ctx: config.Context) { + case ctx.scoped_segments, req.method { + ["current"], http.Get -> handle_current_user(req, ctx) + + _, _ -> wisp.not_found() + } +} diff --git a/src/app/db/connection.gleam b/src/app/db/connection.gleam index 8ba138f..63f2fe9 100644 --- a/src/app/db/connection.gleam +++ b/src/app/db/connection.gleam @@ -36,6 +36,8 @@ pub fn run_query_with( db_conn: sqlight.Connection, model_decoder dcdr, ) { + io.println("---- query start ----") + let prp_stm = sqlite.cake_query_to_prepared_statement(query) let sql = cake.get_sql(prp_stm) |> tap_println let params = cake.get_params(prp_stm) @@ -51,7 +53,7 @@ pub fn run_query_with( NullParam -> sqlight.null() } }) - |> tap_debug("Params: ") + |> tap_debug("with params:: ") sql |> sqlight.query(on: db_conn, with: db_params, expecting: dcdr) } diff --git a/src/app/db/models/user.gleam b/src/app/db/models/user.gleam index b45b296..030c4ab 100644 --- a/src/app/db/models/user.gleam +++ b/src/app/db/models/user.gleam @@ -7,10 +7,8 @@ import cake/select import cake/where import decode/zero import gleam/dynamic -import gleam/io import gleam/list import gleam/result -import gleam/string import sqlight pub type User { @@ -24,8 +22,6 @@ pub type User { } fn decode_user(row: dynamic.Dynamic) { - row |> string.inspect |> io.println - let decoder = { use id <- zero.field(0, zero.int) use name <- zero.field(1, zero.string) @@ -98,6 +94,8 @@ pub fn insert_user( insertable user: InsertableUser, connection conn: sqlight.Connection, ) { + // TODO: think we can get it done with one query, check that out + use _ <- result.try( insert.from_records( records: [user], diff --git a/src/app/hooks/auth.gleam b/src/app/hooks/auth.gleam index ee439df..027b0e6 100644 --- a/src/app/hooks/auth.gleam +++ b/src/app/hooks/auth.gleam @@ -1,7 +1,7 @@ import app/config import app/db/models/user import app/lib/response_helpers -import app/serializers/base +import app/serializers/base_serializer import gleam/result import wisp @@ -21,13 +21,17 @@ pub fn hook( wisp.get_cookie(req, cookie_name, wisp.Signed) |> result.map_error(fn(_) { response_helpers.unauthorized() - |> wisp.json_body(base.serialize_error("Invalid token or token not found")) + |> wisp.json_body(base_serializer.serialize_error( + "Invalid token or token not found", + )) }) |> result.try(fn(user_email) { user.find_by_email(user_email, ctx.db) |> result.map_error(fn(_) { response_helpers.unauthorized() - |> wisp.json_body(base.serialize_error("Invalid token or token not found")) + |> wisp.json_body(base_serializer.serialize_error( + "Invalid token or token not found", + )) }) }) |> result.map(fn(user) { config.set_user(ctx, user) |> handle }) diff --git a/src/app/router.gleam b/src/app/router.gleam index 1147b81..b2853d1 100644 --- a/src/app/router.gleam +++ b/src/app/router.gleam @@ -1,6 +1,7 @@ import app/config import app/controllers/home import app/controllers/sessions +import app/controllers/users import app/hooks/hook import wisp.{type Request, type Response} @@ -10,7 +11,11 @@ pub fn handle_request(req: Request, ctx: config.Context) -> Response { case wisp.path_segments(req) { [] -> home.controller(req) - ["sessions", ..] -> sessions.controller(req, ctx) + ["sessions", ..session_segments] -> + sessions.controller(req, ctx |> config.scope_to(session_segments)) + + ["auth", "users", ..user_segments] -> + users.controller(req, ctx |> config.scope_to(user_segments)) _ -> { wisp.not_found() diff --git a/src/app/serializers/base.gleam b/src/app/serializers/base_serializer.gleam similarity index 100% rename from src/app/serializers/base.gleam rename to src/app/serializers/base_serializer.gleam diff --git a/src/app/serializers/user.gleam b/src/app/serializers/user_serializer.gleam similarity index 88% rename from src/app/serializers/user.gleam rename to src/app/serializers/user_serializer.gleam index 4da67cc..cdec199 100644 --- a/src/app/serializers/user.gleam +++ b/src/app/serializers/user_serializer.gleam @@ -1,5 +1,5 @@ import app/db/models/user -import app/serializers/base +import app/serializers/base_serializer import gleam/json /// here we can do many things. taking inspirations from rails serializers @@ -13,5 +13,5 @@ pub fn run(user: user.User) { #("name", json.string(user.name)), #("created_at", json.string(user.created_at)), ]) - |> base.wrap + |> base_serializer.wrap } diff --git a/test/controllers/home.gleam b/test/controllers/home.gleam new file mode 100644 index 0000000..112f5e9 --- /dev/null +++ b/test/controllers/home.gleam @@ -0,0 +1,40 @@ +import app/router +import gleeunit +import gleeunit/should +import helpers/context +import wisp/testing + +pub fn main() { + gleeunit.main() +} + +pub fn get_home_page_test() { + let request = testing.get("/", []) + let response = router.handle_request(request, context.get_connection()) + + response.status + |> should.equal(200) + + response.headers + |> should.equal([#("content-type", "text/html; charset=utf-8")]) + + response + |> testing.string_body + |> should.equal("Welcome to Okane") +} + +pub fn post_home_page_test() { + let request = testing.post("/", [], "a body") + let response = router.handle_request(request, context.get_connection()) + + response.status + |> should.equal(405) +} + +pub fn page_not_found_test() { + let request = testing.get("/nothing-here", []) + let response = router.handle_request(request, context.get_connection()) + + response.status + |> should.equal(404) +} diff --git a/test/controllers/sessions.gleam b/test/controllers/sessions.gleam new file mode 100644 index 0000000..49f19b4 --- /dev/null +++ b/test/controllers/sessions.gleam @@ -0,0 +1,29 @@ +import app/router +import gleeunit +import gleeunit/should +import helpers/context +import wisp/testing + +pub fn main() { + gleeunit.main() +} + +pub fn sessions_register_missing_form_test() { + let response = + testing.post_form("/sessions/register", [], []) + |> router.handle_request(context.get_connection()) + + response.status |> should.equal(400) +} + +pub fn sessions_register_form_test() { + let response = + testing.post_form("/sessions/register", [], [ + #("name", "zoro"), + #("email", "test@some.com"), + #("password", "123"), + ]) + |> router.handle_request(context.get_connection()) + + response.status |> should.equal(201) +} diff --git a/test/helpers/context.gleam b/test/helpers/context.gleam new file mode 100644 index 0000000..b25efe8 --- /dev/null +++ b/test/helpers/context.gleam @@ -0,0 +1,9 @@ +import app/config +import gleam/option +import sqlight + +pub fn get_connection() -> config.Context { + use conn <- sqlight.with_connection(":memory:") + + config.Context(conn, option.None, []) +} diff --git a/test/okane_test.gleam b/test/okane_test.gleam index 80ad785..142f8eb 100644 --- a/test/okane_test.gleam +++ b/test/okane_test.gleam @@ -1,58 +1,10 @@ -import app/config -import app/router -import gleam/option import gleeunit import gleeunit/should -import sqlight -import wisp/testing pub fn main() { gleeunit.main() } -fn get_connection() -> config.Context { - use conn <- sqlight.with_connection(":memory:") - - config.Context(conn, option.None) -} - -pub fn get_home_page_test() { - let request = testing.get("/", []) - let response = router.handle_request(request, get_connection()) - - response.status - |> should.equal(200) - - response.headers - |> should.equal([#("content-type", "text/html")]) - - response - |> testing.string_body - |> should.equal("Welcome to Okane") -} - -pub fn post_home_page_test() { - let request = testing.post("/", [], "a body") - let response = router.handle_request(request, get_connection()) - - response.status - |> should.equal(405) -} - -pub fn page_not_found_test() { - let request = testing.get("/nothing-here", []) - let response = router.handle_request(request, get_connection()) - - response.status - |> should.equal(404) -} - -pub fn page_session_show_test() { - let request = testing.get("/session", []) - - let response = router.handle_request(request, get_connection()) - - response.status |> should.equal(200) - - response |> testing.string_body |> should.equal("Welcome Okane") +pub fn base_test() { + 10 |> should.equal(10) }