-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" " let validation_result = validation.validate_json(request.body, user_decoder)\n" " \n" " case validation_result {\n" " Ok(user) -> create_user(services.db, user)\n" " Error(err) -> create_validation_error_response(err)\n" " }\n" " }\n" " \n" " fn create_validation_error_response(err: ValidationError) -> Response {\n" " response.json_response(status.bad_request, error_json(err.message))\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", 169). -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", 152). -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", 186). -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", 203). -spec create_generic_error() -> validation_error(). create_generic_error() -> {validation_error, <<"Decode error"/utf8>>, none, none, none}. -file("src/dream/http/validation.gleam", 179). -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", 161). -spec decode_with_decoder( gleam@dynamic:dynamic_(), gleam@dynamic@decode:decoder(ADSH) ) -> {ok, ADSH} | {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", 143). ?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/response.{json_response}\n" " import dream/http/status.{bad_request, created}\n" " import dream/http/validation.{validate_json}\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 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" " json_response(created, post_to_json(created))\n" " }\n" " \n" " Error(err) -> {\n" " let field_json = format_field_as_json(err.field)\n" " let error_json = json.object([\n" " #(\"error\", json.string(err.message)),\n" " #(\"field\", field_json)\n" " ])\n" " json_response(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(ADSD)) -> {ok, ADSD} | {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 ).