Current section
Files
Jump to
Current section
Files
src/dream@http@validation.erl
-module(dream@http@validation).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream/http/validation.gleam").
-export([validate_json/2]).
-export_type([validation_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" JSON validation with structured error messages\n"
"\n"
" Validate and decode JSON request bodies with detailed error information.\n"
" Validation is kept separate from response building - controllers\n"
" decide how to handle validation errors.\n"
"\n"
" ## Quick Example\n"
"\n"
" ```gleam\n"
" import dream/http/validation\n"
" import gleam/dynamic/decode\n"
"\n"
" pub type CreateUser {\n"
" CreateUser(name: String, email: String, age: Int)\n"
" }\n"
"\n"
" let user_decoder = {\n"
" use name <- decode.field(\"name\", decode.string)\n"
" use email <- decode.field(\"email\", decode.string)\n"
" use age <- decode.field(\"age\", decode.int)\n"
" decode.success(CreateUser(name, email, age))\n"
" }\n"
"\n"
" pub fn create(request, context, services) {\n"
" case validation.validate_json(request.body, user_decoder) {\n"
" Ok(user) -> \n"
" // Valid user data, proceed with business logic\n"
" create_user(services.db, user)\n"
" \n"
" Error(err) -> \n"
" // Validation failed, return error response\n"
" response.json_response(\n"
" status.bad_request,\n"
" error_json(err.message)\n"
" )\n"
" }\n"
" }\n"
" ```\n"
"\n"
" ## Error Handling\n"
"\n"
" ValidationError provides detailed information about what went wrong:\n"
" - `message`: Human-readable error message\n"
" - `field`: The JSON field that failed (if applicable)\n"
" - `expected`: What type was expected\n"
" - `found`: What type was actually found\n"
).
-type validation_error() :: {validation_error,
binary(),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary())}.
-file("src/dream/http/validation.gleam", 174).
-spec format_json_error(gleam@json:decode_error()) -> binary().
format_json_error(Error) ->
case Error of
unexpected_end_of_input ->
<<"Unexpected end of JSON input"/utf8>>;
{unexpected_byte, Msg} ->
<<"Unexpected byte: "/utf8, Msg/binary>>;
{unexpected_sequence, Msg@1} ->
<<"Unexpected sequence: "/utf8, Msg@1/binary>>;
{unable_to_decode, Errors} ->
<<"Unable to decode: "/utf8, (gleam@string:inspect(Errors))/binary>>
end.
-file("src/dream/http/validation.gleam", 157).
-spec json_error_to_validation(gleam@json:decode_error()) -> validation_error().
json_error_to_validation(Json_error) ->
{validation_error, format_json_error(Json_error), none, none, none}.
-file("src/dream/http/validation.gleam", 191).
-spec format_single_decode_error(gleam@dynamic@decode:decode_error()) -> validation_error().
format_single_decode_error(Error) ->
{decode_error, Expected, Found, Path} = Error,
Field_path = gleam@string:join(Path, <<"."/utf8>>),
{validation_error,
<<<<<<<<<<"Expected "/utf8, Expected/binary>>/binary,
" but found "/utf8>>/binary,
Found/binary>>/binary,
" at "/utf8>>/binary,
Field_path/binary>>,
{some, Field_path},
{some, Expected},
{some, Found}}.
-file("src/dream/http/validation.gleam", 208).
-spec create_generic_error() -> validation_error().
create_generic_error() ->
{validation_error, <<"Decode error"/utf8>>, none, none, none}.
-file("src/dream/http/validation.gleam", 184).
-spec format_decode_errors(list(gleam@dynamic@decode:decode_error())) -> validation_error().
format_decode_errors(Errors) ->
case gleam@list:first(Errors) of
{ok, Error} ->
format_single_decode_error(Error);
{error, _} ->
create_generic_error()
end.
-file("src/dream/http/validation.gleam", 166).
-spec decode_with_decoder(
gleam@dynamic:dynamic_(),
gleam@dynamic@decode:decoder(ADIA)
) -> {ok, ADIA} | {error, validation_error()}.
decode_with_decoder(Json_obj, Decoder) ->
_pipe = gleam@dynamic@decode:run(Json_obj, Decoder),
gleam@result:map_error(_pipe, fun format_decode_errors/1).
-file("src/dream/http/validation.gleam", 148).
?DOC(
" Validate and decode JSON request body\n"
"\n"
" Parses JSON string and decodes it using the provided decoder.\n"
" Returns the decoded data or a detailed validation error.\n"
"\n"
" This function is generic over the return type - use `gleam/dynamic/decode`\n"
" to define decoders for your types.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import dream/http/validation\n"
" import dream/http/response\n"
" import dream/http/status\n"
" import gleam/dynamic/decode\n"
" import gleam/json\n"
"\n"
" pub type CreatePost {\n"
" CreatePost(title: String, body: String, published: Bool)\n"
" }\n"
"\n"
" let post_decoder = {\n"
" use title <- decode.field(\"title\", decode.string)\n"
" use body <- decode.field(\"body\", decode.string)\n"
" use published <- decode.field(\"published\", decode.bool)\n"
" decode.success(CreatePost(title, body, published))\n"
" }\n"
"\n"
" pub fn create_post(request, context, services) {\n"
" case validation.validate_json(request.body, post_decoder) {\n"
" Ok(post) -> {\n"
" // Valid post data - proceed with business logic\n"
" let created = insert_post(services.db, post)\n"
" response.json_response(status.created, post_to_json(created))\n"
" }\n"
" \n"
" Error(err) -> {\n"
" // Validation failed - return error response\n"
" let error_json = json.object([\n"
" #(\"error\", json.string(err.message)),\n"
" #(\"field\", case err.field {\n"
" Some(f) -> json.string(f)\n"
" None -> json.null()\n"
" })\n"
" ])\n"
" response.json_response(status.bad_request, json.to_string(error_json))\n"
" }\n"
" }\n"
" }\n"
" ```\n"
"\n"
" ## Error Cases\n"
"\n"
" - Invalid JSON syntax: Returns error with parse details\n"
" - Missing required field: Returns error with field path\n"
" - Wrong type: Returns error with expected vs. found types\n"
" - Invalid enum value: Returns error with validation details\n"
).
-spec validate_json(binary(), gleam@dynamic@decode:decoder(ADHW)) -> {ok, ADHW} |
{error, validation_error()}.
validate_json(Body, Decoder) ->
_pipe = gleam@json:parse(
Body,
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
_pipe@1 = gleam@result:map_error(_pipe, fun json_error_to_validation/1),
gleam@result:'try'(
_pipe@1,
fun(_capture) -> decode_with_decoder(_capture, Decoder) end
).