midas-framework / midas

A framework for Gleam, Midas makes shiny things.
https://hex.pm/packages/midas
170 stars 5 forks source link

Input Parsing and error handling #14

Open CrowdHailer opened 4 years ago

CrowdHailer commented 4 years ago

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 commented 4 years ago

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

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 commented 4 years ago

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 commented 4 years ago

@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

lpil commented 4 years ago

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