From 8beb4adf9dce37a6f0617774764c70c41e2c6c85 Mon Sep 17 00:00:00 2001 From: custompro98 Date: Thu, 11 Jul 2024 13:51:36 -0400 Subject: [PATCH] feat: Return an error when buildng a request with missing required fields --- README.md | 4 ++ gleam.toml | 2 +- src/glibsql/http.gleam | 123 +++++++++++++++++++++-------------- src/glibsql/libsql.gleam | 1 + test/glibsql/http_test.gleam | 30 +++++---- 5 files changed, 99 insertions(+), 61 deletions(-) create mode 100644 src/glibsql/libsql.gleam diff --git a/README.md b/README.md index b7902ee..9e9add8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ pub fn main() { )) |> glibsql.build + let assert Ok(request) = request + use response <- result.try(httpc.send(request)) // ... @@ -50,6 +52,8 @@ pub fn main() { |> glibsql.with_baton("") |> glibsql.build + let assert Ok(second_request) = second_request + use response <- result.try(httpc.send(second_request)) } diff --git a/gleam.toml b/gleam.toml index c69e8b9..bcce292 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "glibsql" -version = "0.3.0" +version = "0.4.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. diff --git a/src/glibsql/http.gleam b/src/glibsql/http.gleam index d5841ed..6176130 100644 --- a/src/glibsql/http.gleam +++ b/src/glibsql/http.gleam @@ -8,6 +8,7 @@ import gleam/http/request as http_request import gleam/json import gleam/list import gleam/option.{type Option, None, Some} +import gleam/result import gleam/string /// Statement wraps the supported types of requests. @@ -26,32 +27,38 @@ pub type Statement { CloseStatement } -/// Request encapsulates everything needed to execute +/// HttpRequest encapsulates everything needed to execute /// a Hrana over HTTP libSQL request. /// /// see `new_request()` to construct this record. -pub opaque type Request { - Request( - database: String, - organization: String, - host: String, - path: String, - token: String, +pub opaque type HttpRequest { + HttpRequest( + database: Option(String), + organization: Option(String), + host: Option(String), + path: Option(String), + token: Option(String), statements: List(Statement), baton: Option(String), ) } +/// Error type for all possible errors returned by glibsql/http. +pub opaque type GlibsqlError { + /// Raised when a required property is not provided. + MissingPropertyError(String) +} + /// Create a new Hrana over HTTP libSQL request. /// /// Uses the builder pattern to construct everything necessary to send a request. -pub fn new_request() -> Request { - Request( - database: "", - organization: "", - host: "turso.io", - path: "/v2/pipeline", - token: "", +pub fn new_request() -> HttpRequest { + HttpRequest( + database: None, + organization: None, + host: None, + path: None, + token: None, statements: [], baton: None, ) @@ -62,8 +69,8 @@ pub fn new_request() -> Request { /// /// Given a Turso databse URL like libsql://example-database-myorganization.turso.io /// The database name is "example-database" -pub fn with_database(request: Request, database: String) -> Request { - Request(..request, database: database) +pub fn with_database(request: HttpRequest, database: String) -> HttpRequest { + HttpRequest(..request, database: Some(database)) } /// Set the target database organization. @@ -72,10 +79,10 @@ pub fn with_database(request: Request, database: String) -> Request { /// Given a Turso databse URL like libsql://example-database-myorganization.turso.io /// The database name is "myorganization" pub fn with_organization( - request: Request, + request: HttpRequest, organization: String, -) -> Request { - Request(..request, organization: organization) +) -> HttpRequest { + HttpRequest(..request, organization: Some(organization)) } /// Set the target database host. @@ -84,61 +91,81 @@ pub fn with_organization( /// /// Given a Turso databse URL like libsql://example-database-myorganization.turso.io /// The host name is "turso.io" -pub fn with_host(request: Request, host: String) -> Request { - Request(..request, host: host) +pub fn with_host(request: HttpRequest, host: String) -> HttpRequest { + HttpRequest(..request, host: Some(host)) } /// Set the target database path on the host. /// NOTE: this defaults to Turso's /v2/pipeline /// Calling this function multiple times will override the previous value. -pub fn with_path(request: Request, path: String) -> Request { - Request(..request, path: path) +pub fn with_path(request: HttpRequest, path: String) -> HttpRequest { + HttpRequest(..request, path: Some(path)) } /// Set the Bearer token to access the database. Do not include `Bearer `. /// Calling this function multiple times will override the previous value. -pub fn with_token(request: Request, token: String) -> Request { - Request(..request, token: token) +pub fn with_token(request: HttpRequest, token: String) -> HttpRequest { + HttpRequest(..request, token: Some(token)) } /// Set a statement on the request. /// This function may be called multiple times, additional statements will be /// executed in order. -pub fn with_statement(request: Request, statement: Statement) -> Request { - Request(..request, statements: [statement, ..request.statements]) +pub fn with_statement(request: HttpRequest, statement: Statement) -> HttpRequest { + HttpRequest(..request, statements: [statement, ..request.statements]) } /// Clear all statements from the request. -pub fn clear_statements(request: Request) -> Request { - Request(..request, statements: []) +pub fn clear_statements(request: HttpRequest) -> HttpRequest { + HttpRequest(..request, statements: []) } /// Set the baton from a previous connection to be reused. -pub fn with_baton(request: Request, baton: String) -> Request { - Request(..request, baton: Some(baton)) +pub fn with_baton(request: HttpRequest, baton: String) -> HttpRequest { + HttpRequest(..request, baton: Some(baton)) } /// Build the request using the previously provided values. /// Returns a gleam/http request suitable to be used in your HTTP client of choice. -pub fn build(request: Request) -> http_request.Request(String) { - http_request.new() - |> http_request.set_method(http.Post) - |> http_request.set_scheme(http.Https) - |> http_request.set_host( - request.database <> "-" <> request.organization <> "." <> request.host, - ) - |> http_request.set_path(request.path) - |> http_request.set_header( - "Authorization", - string.join(["Bearer", request.token], " "), +pub fn build( + request: HttpRequest, +) -> Result(http_request.Request(String), GlibsqlError) { + use database <- result.try(option.to_result( + request.database, + MissingPropertyError("database"), + )) + + use organization <- result.try(option.to_result( + request.organization, + MissingPropertyError("organization"), + )) + + let host = option.unwrap(request.host, "turso.io") + let path = option.unwrap(request.path, "/v2/pipeline") + + use token <- result.try(option.to_result( + request.token, + MissingPropertyError("token"), + )) + + Ok( + http_request.new() + |> http_request.set_method(http.Post) + |> http_request.set_scheme(http.Https) + |> http_request.set_host(database <> "-" <> organization <> "." <> host) + |> http_request.set_path(path) + |> http_request.set_header( + "Authorization", + string.join(["Bearer", token], " "), + ) + |> http_request.set_header("Content-Type", "application/json") + |> http_request.set_header("Accept", "application/json") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") + |> http_request.set_body(build_json(request)), ) - |> http_request.set_header("Content-Type", "application/json") - |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") - |> http_request.set_body(build_json(request)) } -fn build_json(req: Request) { +fn build_json(req: HttpRequest) { let statements = list.reverse(req.statements) |> list.map(fn(stmt) { diff --git a/src/glibsql/libsql.gleam b/src/glibsql/libsql.gleam new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/glibsql/libsql.gleam @@ -0,0 +1 @@ + diff --git a/test/glibsql/http_test.gleam b/test/glibsql/http_test.gleam index 522e735..9708331 100644 --- a/test/glibsql/http_test.gleam +++ b/test/glibsql/http_test.gleam @@ -13,7 +13,7 @@ pub fn builder_custom_host_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") |> http_request.set_body("{\"baton\":null,\"requests\":[]}") glibsql.new_request() @@ -23,7 +23,7 @@ pub fn builder_custom_host_test() { |> glibsql.with_path("/v1/acme") |> glibsql.with_token("token") |> glibsql.build - |> should.equal(expected) + |> should.equal(Ok(expected)) } pub fn builder_no_statements_test() { @@ -36,7 +36,7 @@ pub fn builder_no_statements_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") |> http_request.set_body("{\"baton\":null,\"requests\":[]}") glibsql.new_request() @@ -44,7 +44,7 @@ pub fn builder_no_statements_test() { |> glibsql.with_organization("organization") |> glibsql.with_token("token") |> glibsql.build - |> should.equal(expected) + |> should.equal(Ok(expected)) } pub fn builder_single_statement_test() { @@ -57,7 +57,7 @@ pub fn builder_single_statement_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") |> http_request.set_body( "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users\"}},{\"type\":\"close\"}]}", ) @@ -69,7 +69,7 @@ pub fn builder_single_statement_test() { |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM users")) |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.build - |> should.equal(expected) + |> should.equal(Ok(expected)) } pub fn builder_many_statement_test() { @@ -82,7 +82,7 @@ pub fn builder_many_statement_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") |> http_request.set_body( "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users\"}},{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM posts\"}},{\"type\":\"close\"}]}", ) @@ -95,7 +95,7 @@ pub fn builder_many_statement_test() { |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM posts")) |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.build - |> should.equal(expected) + |> should.equal(Ok(expected)) } pub fn builder_clear_statements_test() { @@ -108,7 +108,7 @@ pub fn builder_clear_statements_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") |> http_request.set_body("{\"baton\":null,\"requests\":[]}") glibsql.new_request() @@ -120,7 +120,7 @@ pub fn builder_clear_statements_test() { |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.clear_statements |> glibsql.build - |> should.equal(expected) + |> should.equal(Ok(expected)) } pub fn builder_baton_test() { @@ -133,7 +133,7 @@ pub fn builder_baton_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.3.0") + |> http_request.set_header("User-Agent", "glibsql/0.4.0") |> http_request.set_body( "{\"baton\":\"baton\",\"requests\":[{\"type\":\"close\"}]}", ) @@ -145,5 +145,11 @@ pub fn builder_baton_test() { |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.with_baton("baton") |> glibsql.build - |> should.equal(expected) + |> should.equal(Ok(expected)) +} + +pub fn builder_missing_fields_test() { + glibsql.new_request() + |> glibsql.build + |> should.be_error }