midas icon indicating copy to clipboard operation
midas copied to clipboard

Input Parsing and error handling

Open CrowdHailer opened this issue 5 years ago • 4 comments

Add an example endpoint that parses input.

This will probably make use of https://github.com/rjdellecese/gleam_decode and ideally the approach would be reusable for multiple input sources. e.g. query strings, JSON, forms

CrowdHailer avatar Apr 29 '20 15:04 CrowdHailer

Running through some examples for input handling.

In all these cases the error type is

pub type Invalid(a) {
  Missing(key: String)
  CastFailure(key: String, help: a)
}

Keep as lean as possible.

In these examples all the custom as_* functions return the same error type, which will end up as the content in help.

// required field
try name = params.find(form, "name")
try name = params.cast(form, as_name)

// optional field
let url = params.optional(form, "url")
try url = params.cast_optional(url, url.parse) |> result.map_error(fn(_) { "not a valid url"})

// or wrapper function that handles mapping the error
try url = params.cast_optional(url, as_url)

// handling a fallback
let url = params.use_fallback(url, Uri(...))
Notes
  • each variable name shows up twice and has a different type in each case, first as a raw string then as a cast value.
  • functions like cast cast_optional need to be given key value for writing debug messages.

Higher level

try name = params.required(from: form, get: "name", cast: as_name)
try url = params.optional(from: form, get: "url", cast: as_url)
try url = params.overridable(from: form, get: "url", cast: as_url, or: "default.com")

Better as_ functions

A params module could define cast functions for strings ints etc

try name = params.required(form, "name", as_string(_, [MinLength(5), Pattern(), MaxLength(30)])

Comment on try syntax

If this change existed

let name = try params.required(...
// instead of
try name = params.required(...

Then you could construct form objects really easily

fn cast_form(form) -> Result(CreateUser, params.Invalid) {
  Ok(CreateUser(
    name: try params.required(form, "name", as_name),
    url: try params.overridable(form, "url", as_url, "home.com")
  ))
} 

CrowdHailer avatar Jun 07 '20 14:06 CrowdHailer

Wrapping Errors

Instead of wrapper functions that all return the same error type we could have a function for parse_form and then wrap the error once.

fn try_parse_form(form) {
  try ...
  try ...
  FormValues(a: ....)
}

pub fn parse_form(form) {
  try_parse_form(form)
  |> result.map_error()
}

There will be a lot of these function pairs. because there is no way to wrap the error inside the first function.

Currently I prefer writting web wrapper functions, but this could be a lot of wrapper functions and requires more up front work about choosing an error type.

Application Error type

handle functions return Result(Response, Error) The error could be another response but this makes it too easy to encode the error in different ways.

It could a big enum

type ErrorResponse {
  UnprocessableEntity(input.Invalid())
}

fn parse_form(form) {
  try_parse_form(form)
  // big enum
  |> result.map_error(UnprocessableEntity)
}

There is no completeness to assure that Enum is used correctly. Also there might be alot of time designing each sub type. Nice to use in parse_form func tho, see example above

Or it could be a special Error type

type ErrorResponse {
  ErrorResponse(type: ErrorType, details: String)
}

can enum all the error types, has only a single place to set the details.

CrowdHailer avatar Jun 08 '20 06:06 CrowdHailer

@lpil thanks for the feedback on the PR, I have merged version 1 to play with will see how it goes. Am mulling some of your suggestions for a next update.

I particularly think it might make sense to work on maps because although params come in order the api as designed so far doesn't allow you to pull out a value that is defined multiple times

CrowdHailer avatar Jun 11 '20 13:06 CrowdHailer

Perhaps we could move forward with this and a more complex validation library can be left as an exercise for someone else. Getting 90% of the way there with everything provides the most value now!

Thanks

lpil avatar Jun 11 '20 14:06 lpil