-
Notifications
You must be signed in to change notification settings - Fork 0
/
validate_monadic.gleam
188 lines (178 loc) · 5.8 KB
/
validate_monadic.gleam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import gleam/list
import gleam/result
/// Simple type alias over a Result with a *non-empty* list off generic errors
/// for the Error branch. The non-empty list is important here. For validation we must
/// represent a Result type that can have multiple errors, but we must avoid allowing
/// something like `Error([])` in which we can have an error branch but no errors!
pub type Validation(validated, error) =
Result(validated, ErrorList(error))
/// Simple type alias for a non-empty list. A non-empty list is just a structure containing the
/// first item of the list, followed by the rest of the list. You can find a more
/// useful implementation [here](https://hexdocs.pm/non_empty_list/non_empty_list.html#NonEmptyList)
/// . For the purposes of not including an extra dependency, we just use a tuple here.
pub type ErrorList(error) =
#(error, List(error))
/// Convenience function for lifting a single error into our non-empty list error.
pub fn error(err: error) -> Validation(a, error) {
Error(#(err, []))
}
/// Convenience function for lifting a value into our validation type's `Ok` branch, like other methods
/// in this module, it is literally just an alias for a `Result` type method.
pub fn succeed(a) -> Validation(a, error) {
Ok(a)
}
/// Map a validation type to another type. This is often useful to nest the result of a validation
/// into a "ValidatedType". This is just an alias over `result.map`
///
/// ```gleam
/// pub type ValidatedLastName {
/// ValidatedLastName(String)
/// }
///
/// // ...
///
/// let last_name_result =
/// form.last_name
/// |> string_non_empty
/// |> validate.map(ValidatedLastName)
/// ```
pub fn map(
over validation: Validation(a, error),
with map_fn: fn(a) -> b,
) -> Validation(b, error) {
result.map(validation, map_fn)
}
/// Map over all the errors for a validation result.
/// This is very useful for cases where you have re-usable validators with generic error messages,
/// and you wish to specify the errors are associated with a specific field
///
/// ```gleam
///
/// let validation_result =
/// raw_field
/// |> validate.compose(no_numbers, [shorter_than_10])
/// |> validate.map_error(string.append("Field Name Error: ", _))
/// ```
pub fn map_error(
over validation: Validation(a, error_a),
with map_fn: fn(error_a) -> error_b,
) -> Validation(a, error_b) {
case validation {
Ok(v) -> Ok(v)
Error(#(head, rest)) -> {
Error(#(map_fn(head), list.map(rest, map_fn)))
}
}
}
/// Compose together multiple validations. This combine the errors of all validations that fail,
/// and does not stop at the first failure. Takes the input to be validated as the first argument,
/// then a non-empty list of unary functions that take that input and return a validation result
/// of the same type.
///
///
/// ```gleam
///
/// let validation_result =
/// raw_field
/// |> validate.compose(no_numbers, [shorter_than_10, no_symbols, no_whitespace])
/// ```
pub fn compose(
input: a,
validation: fn(a) -> Validation(b, error),
validations: List(fn(a) -> Validation(b, error)),
) -> Validation(b, error) {
list.fold(validations, validation(input), fn(acc, cur) {
and_also(acc, cur(input))
})
}
/// Combine two validation results into one. This mainly for merging errors. The `Ok `branch of the
/// last validation supplied will be the returned `Ok` branch. This is used internally by `compose`
pub fn and_also(
validation_a: Validation(a, error),
validation_b: Validation(a, error),
) -> Validation(a, error) {
case validation_a, validation_b {
Ok(_), Ok(a) -> Ok(a)
Error(#(err_a_head, err_a_rest)), Error(#(err_b_head, err_b_rest)) -> {
Error(#(
err_a_head,
list.concat([err_a_rest, list.prepend(err_b_rest, err_b_head)]),
))
}
Error(err), _ -> Error(err)
_, Error(err) -> Error(err)
}
}
/// Specify a validation that will run after a given validation, using its result. This is very
/// useful for validations that need to run after a transform is attempted.
///
/// ```gleam
/// let age_result =
/// form.age_string
/// |> is_parsable_int
/// |> validate.and_then(int_less_than(_, 101))
/// ```
///
/// It can easily be used in conjuction with `compose`
///
/// ```gleam
/// let age_result =
/// form.age_string
/// |> is_parsable_int
/// |> validate.and_then(
/// validate.compose(int_less_than(_, 101), [int_greater_than(_, 0)])
/// )
/// ```
pub fn and_then(
over validation: Validation(a, error),
bind bind_fn: fn(a) -> Validation(b, error),
) -> Validation(b, error) {
result.then(validation, bind_fn)
}
/// Used to create applicative chains of validation. This is very important for combining validation
/// of fields into the validation of an entire form.
///
/// ```gleam
/// let validate_form = function.curry3(fn (
/// ValidFirstName,
/// ValidLastName,
/// ValidAge
/// ) {
/// ValidForm(ValidFirstName, ValidLastName, ValidAge)
/// })
///
/// let validation_result =
/// validate.succeed(validate_form)
/// |> validate.and_map(first_name_result)
/// |> validate.and_map(last_name_result)
/// |> validate.and_map(age_result)
/// ```
pub fn and_map(
prev prev_validation: Validation(fn(a) -> b, error),
next validation: Validation(a, error),
) -> Validation(b, error) {
case prev_validation {
Ok(apply) -> {
case validation {
Ok(a) -> Ok(apply(a))
Error(err) -> Error(err)
}
}
Error(#(prev_err_head, prev_err_rest)) -> {
case validation {
Ok(_) -> {
Error(#(prev_err_head, prev_err_rest))
}
Error(#(next_err_head, next_err_rest)) -> {
Error(#(
prev_err_head,
list.flatten([
prev_err_rest,
list.prepend(next_err_rest, next_err_head),
]),
))
}
}
}
}
}