├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── gleam.toml ├── manifest.toml ├── src ├── decode.gleam ├── decode │ └── zero.gleam ├── decode_ffi.erl └── decode_ffi.mjs └── test ├── decode └── zero_test.gleam ├── decode_test.gleam └── decode_zero_test_ffi.mjs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: "27" 18 | gleam-version: "1.5.0" 19 | rebar3-version: "3" 20 | # elixir-version: "1.15.4" 21 | - run: gleam deps download 22 | - run: gleam test --target erlang 23 | - run: gleam test --target javascript 24 | - run: gleam format --check src test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.1.0 - 2024-12-19 4 | 5 | - Added the `zero.recursive` function. 6 | 7 | ## v1.0.0 - 2024-12-10 8 | 9 | - `zero.dict` can now decode JavaScript maps, weakmaps, and objects without a 10 | prototype. 11 | 12 | ## v0.5.0 - 2024-11-15 13 | 14 | - The `field`, `subfield`, and `at` functions in `decode/zero` no longer return 15 | a default value when a field is not present, instead an error is returned. 16 | - `decode/zero` module gains the `optional_field` and `optionally_at` functions. 17 | - Fixed a bug in `decode/zero` with indexing with `0` on JavaScript. 18 | 19 | ## v0.4.1 - 2024-11-01 20 | 21 | - Fixed spelling of `new_primitive_decoder`. 22 | 23 | ## v0.4.0 - 2024-10-31 24 | 25 | - The `decode/zero` module gains `new_primitive_decoder` function. 26 | 27 | ## v0.3.0 - 2024-09-18 28 | 29 | - Added `decode/zero`, a module that provides a new decoder API. 30 | 31 | ## v0.2.1 - 2024-09-01 32 | 33 | - Fixed JavaScript support. 34 | 35 | ## v0.2.0 - 2024-06-30 36 | 37 | - Added the `subfield` function. 38 | - Fixed a bug where `field` would not index into tuples. 39 | 40 | ## v0.1.0 - 2024-06-29 41 | 42 | - Initial release. 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # decode 2 | 3 | Ergonomic dynamic decoders for Gleam! 4 | 5 | [![Package Version](https://img.shields.io/hexpm/v/decode)](https://hex.pm/packages/decode) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/decode/) 7 | 8 | ```sh 9 | gleam add decode 10 | ``` 11 | ```gleam 12 | import decode/zero as decode 13 | import gleam/dynamic.{type Dynamic} 14 | 15 | 16 | pub type User { 17 | User(name: String, email: String, is_admin: Bool) 18 | } 19 | 20 | /// Decode data of this shape into a `User` record. 21 | /// 22 | /// { 23 | /// "name" -> "Lucy", 24 | /// "email" -> "lucy@example.com", 25 | /// "is-admin" -> true 26 | /// } 27 | /// 28 | pub fn run(data: Dynamic) { 29 | let decoder = { 30 | use name <- decode.field("name", decode.string) 31 | use email <- decode.field("email", decode.string) 32 | use is_admin <- decode.field("is-admin", decode.bool) 33 | decode.success(User(name:, email:, is_admin:)) 34 | } 35 | 36 | decode.run(data, decoder) 37 | } 38 | ``` 39 | Or, alternatively: 40 | ```gleam 41 | import decode 42 | import gleam/dynamic.{type Dynamic} 43 | 44 | pub type User { 45 | User(name: String, email: String, is_admin: Bool) 46 | } 47 | 48 | /// Decode data of this shape into a `User` record. 49 | /// 50 | /// { 51 | /// "name" -> "Lucy", 52 | /// "email" -> "lucy@example.com", 53 | /// "is-admin" -> true 54 | /// } 55 | /// 56 | pub fn run(data: Dynamic) { 57 | let decoder = 58 | decode.into({ 59 | use name <- decode.parameter 60 | use email <- decode.parameter 61 | use is_admin <- decode.parameter 62 | User(name, email, is_admin) 63 | }) 64 | |> decode.field("name", decode.string) 65 | |> decode.field("email", decode.string) 66 | |> decode.field("is-admin", decode.bool) 67 | 68 | decoder 69 | |> decode.from(data) 70 | } 71 | ``` 72 | For more documentation and examples view . 73 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "decode" 2 | version = "1.1.0" 3 | description = "Ergonomic dynamic decoders for Gleam!" 4 | licences = ["Apache-2.0"] 5 | repository = { type = "github", user = "lpil", repo = "decode" } 6 | links = [{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }] 7 | 8 | [dependencies] 9 | gleam_stdlib = ">= 0.34.0 and < 2.0.0" 10 | 11 | [dev-dependencies] 12 | gleeunit = ">= 1.0.0 and < 2.0.0" 13 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, 6 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 7 | ] 8 | 9 | [requirements] 10 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 11 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 12 | -------------------------------------------------------------------------------- /src/decode.gleam: -------------------------------------------------------------------------------- 1 | //// The `Dynamic` type is used to represent dynamically typed data. That is, data 2 | //// that we don't know the precise type of yet, so we need to introspect the data to 3 | //// see if it is of the desired type before we can use it. Typically data like this 4 | //// would come from user input or from untyped languages such as Erlang or JavaScript. 5 | //// 6 | //// This module provides the `Decoder` type and associated functions, which provides 7 | //// a type-safe and composable way to convert dynamic data into some desired type, 8 | //// or into errors if the data doesn't have the desired structure. 9 | //// 10 | //// The `Decoder` type is generic and has 1 type parameter, which is the type that 11 | //// it attempts to decode. A `Decoder(String)` can be used to decode strings, and a 12 | //// `Decoder(Option(Int))` can be used to decode `Option(Int)`s 13 | //// 14 | //// Decoders work using _runtime reflection_ and the data structures of the target 15 | //// platform. Differences between Erlang and JavaScript data structures may impact 16 | //// your decoders, so it is important to test your decoders on all supported 17 | //// platforms. 18 | //// 19 | //// # Examples 20 | //// 21 | //// Dynamic data may come from various sources and so many different syntaxes could 22 | //// be used to describe or construct them. In these examples the JSON syntax is 23 | //// largely used, and you can apply the same techniques to data from any source. 24 | //// 25 | //// ## Simple types 26 | //// 27 | //// This module defines decoders for simple data types such as [`string`](#string), 28 | //// [`int`](#int), [`float`](#float), [`bit_array`](#bit_array), and [`bool`](#bool). 29 | //// 30 | //// ```gleam 31 | //// // Data: 32 | //// // "Hello, Joe!" 33 | //// 34 | //// let result = 35 | //// decode.string 36 | //// |> decode.from(data) 37 | //// 38 | //// let assert Ok("Hello, Joe!") = result 39 | //// ``` 40 | //// 41 | //// ## Lists 42 | //// 43 | //// The [`list`](#list) decoder decodes `List`s. To use it you must construct it by 44 | //// passing in another decoder into the `list` function, which is the decoder that 45 | //// is to be used for the elements of the list, type checking both the list and its 46 | //// elements. 47 | //// 48 | //// ```gleam 49 | //// // Data: 50 | //// // [1, 2, 3, 4] 51 | //// 52 | //// let result = 53 | //// decode.list(decode.int) 54 | //// |> decode.from(data) 55 | //// 56 | //// let assert Ok([1, 2, 3, 4]) = result 57 | //// ``` 58 | //// 59 | //// On Erlang this decoder can decode from lists, and on JavaScript it can decode 60 | //// from lists as well as JavaScript arrays. 61 | //// 62 | //// ## Options 63 | //// 64 | //// The [`optional`](#optional) decoder is used to decode values that may or may not 65 | //// be present. In other environment these might be called "nullable" values. 66 | //// 67 | //// Like the `list` decoder, the `optional` decoder takes another decoder, 68 | //// which is used to decode the value if it is present. 69 | //// 70 | //// ```gleam 71 | //// // Data: 72 | //// // 12.45 73 | //// 74 | //// let result = 75 | //// decode.optional(decode.int) 76 | //// |> decode.from(data) 77 | //// 78 | //// let assert Ok(option.Some(12.45)) = result 79 | //// ``` 80 | //// ```gleam 81 | //// // Data: 82 | //// // null 83 | //// 84 | //// let result = 85 | //// decode.optional(decode.int) 86 | //// |> decode.from(data) 87 | //// 88 | //// let assert Ok(option.None) = result 89 | //// ``` 90 | //// 91 | //// This decoder knows how to handle multiple different runtime representations of 92 | //// absent values, including `Nil`, `None`, `null`, and `undefined`. 93 | //// 94 | //// ## Dicts 95 | //// 96 | //// The [`dict`](#dict) decoder decodes `Dicts` and contains two other decoders, one 97 | //// for the keys, one for the values. 98 | //// 99 | //// ```gleam 100 | //// // Data: 101 | //// // { "Lucy": 10, "Nubi": 20 } 102 | //// 103 | //// let result = 104 | //// decode.dict(decode.string, decode.int) 105 | //// |> decode.from(data) 106 | //// 107 | //// let assert Ok(dict.from_list([#("Lucy", 10), #("Nubi": 20)])) = result 108 | //// ``` 109 | //// 110 | //// ## Indexing objects 111 | //// 112 | //// The [`at`](#at) decoder can be used to decode a value that is nested within 113 | //// key-value containers such as Gleam dicts, Erlang maps, or JavaScript objects. 114 | //// 115 | //// ```gleam 116 | //// // Data: 117 | //// // { "one": { "two": 123 } } 118 | //// 119 | //// let result = 120 | //// decode.at(["one", "two"], decode.int) 121 | //// |> decode.from(data) 122 | //// 123 | //// let assert Ok(123) = result 124 | //// ``` 125 | //// 126 | //// ## Indexing arrays 127 | //// 128 | //// If you use ints as keys then the [`at`](#at) decoder can be used to index into 129 | //// array-like containers such as Gleam or Erlang tuples, or JavaScript arrays. 130 | //// 131 | //// ```gleam 132 | //// // Data: 133 | //// // ["one", "two", "three"] 134 | //// 135 | //// let result = 136 | //// decode.at([1], decode.string) 137 | //// |> decode.from(data) 138 | //// 139 | //// let assert Ok("two") = result 140 | //// ``` 141 | //// 142 | //// ## Records 143 | //// 144 | //// Decoding records from dynamic data is more complex and requires combining a 145 | //// decoder for each field and a special constructor that builds your records with 146 | //// the decoded field values. 147 | //// 148 | //// ```gleam 149 | //// // Data: 150 | //// // { 151 | //// // "score": 180, 152 | //// // "name": "Mel Smith", 153 | //// // "is-admin": false, 154 | //// // "enrolled": true, 155 | //// // "colour": "Red", 156 | //// // } 157 | //// 158 | //// let result = 159 | //// decode.into({ 160 | //// use name <- decode.parameter 161 | //// use score <- decode.parameter 162 | //// use colour <- decode.parameter 163 | //// use enrolled <- decode.parameter 164 | //// Player(name: name, score: score, colour: colour, enrolled: enrolled) 165 | //// }) 166 | //// |> decode.field("name", decode.string) 167 | //// |> decode.field("score", decode.int) 168 | //// |> decode.field("colour", decode.string) 169 | //// |> decode.field("enrolled", decode.bool) 170 | //// |> decode.from(data) 171 | //// 172 | //// let assert Ok(Player("Mel Smith", 180, "Red", True)) = result 173 | //// ``` 174 | //// 175 | //// The ordering of the parameters defined with the `parameter` function must match 176 | //// the ordering of the decoders used with the `field` function. 177 | //// 178 | //// ## Enum variants 179 | //// 180 | //// Imagine you have a custom type where all the variants do not contain any values. 181 | //// 182 | //// ```gleam 183 | //// pub type PocketMonsterType { 184 | //// Fire 185 | //// Water 186 | //// Grass 187 | //// Electric 188 | //// } 189 | //// ``` 190 | //// 191 | //// You might choose to encode these variants as strings, `"fire"` for `Fire`, 192 | //// `"water"` for `Water`, and so on. To decode them you'll need to decode the dynamic 193 | //// data as a string, but then you'll need to decode it further still as not all 194 | //// strings are valid values for the enum. This can be done with the `then` 195 | //// function, which enables running a second decoder after the first one 196 | //// succeeds. 197 | //// 198 | //// ```gleam 199 | //// let decoder = 200 | //// decode.string 201 | //// |> decode.then(fn(decoded_string) { 202 | //// case decoded_string { 203 | //// // Return succeeding decoders for valid strings 204 | //// "fire" -> decode.into(Fire) 205 | //// "water" -> decode.into(Water) 206 | //// "grass" -> decode.into(Grass) 207 | //// "electric" -> decode.into(Electric) 208 | //// // Return a failing decoder for any other strings 209 | //// _ -> decode.fail("PocketMonsterType") 210 | //// } 211 | //// }) 212 | //// 213 | //// decoder 214 | //// |> decode.from(dynamic.from("water")) 215 | //// // -> Ok(Water) 216 | //// 217 | //// decoder 218 | //// |> decode.from(dynamic.from("wobble")) 219 | //// // -> Error([DecodeError("PocketMonsterType", "String", [])]) 220 | //// ``` 221 | //// 222 | //// ## Record variants 223 | //// 224 | //// Decoding type variants that contain other values is done by combining the 225 | //// techniques from the "enum variants" and "records" examples. Imagine you have 226 | //// this custom type that you want to decode: 227 | //// 228 | //// ```gleam 229 | //// pub type PocketMonsterPerson { 230 | //// Trainer(name: String, badge_count: Int) 231 | //// GymLeader(name: String, speciality: PocketMonsterType) 232 | //// } 233 | //// ``` 234 | //// And you would like to be able to decode these from JSON documents like these. 235 | //// ```json 236 | //// { 237 | //// "type": "trainer", 238 | //// "name": "Ash", 239 | //// "badge-count": 1, 240 | //// } 241 | //// ``` 242 | //// ```json 243 | //// { 244 | //// "type": "gym-leader", 245 | //// "name": "Misty", 246 | //// "speciality": "water", 247 | //// } 248 | //// ``` 249 | //// 250 | //// Notice how both documents have a `"type"` field, which is used to indicate which 251 | //// variant the data is for. 252 | //// 253 | //// First, define decoders for each of the variants: 254 | //// 255 | //// ```gleam 256 | //// let trainer_decoder = 257 | //// decode.into({ 258 | //// use name <- decode.parameter 259 | //// use badge_count <- decode.parameter 260 | //// Trainer(name, badge_count) 261 | //// }) 262 | //// |> decode.field("name", decode.string) 263 | //// |> decode.field("badge-count", decode.int) 264 | //// 265 | //// let gym_leader_decoder = 266 | //// decode.into({ 267 | //// use name <- decode.parameter 268 | //// use speciality <- decode.parameter 269 | //// GymLeader(name, speciality) 270 | //// }) 271 | //// |> decode.field("name", decode.string) 272 | //// |> decode.field("speciality", pocket_monster_type_decoder) 273 | //// ``` 274 | //// 275 | //// A third decoder can be used to extract and decode the `"type"` field, and the 276 | //// `then` function then returns whichever decoder is suitable for the document. 277 | //// 278 | //// ```gleam 279 | //// let decoder = 280 | //// decode.at(["type"], decode.string) 281 | //// |> decode.then(fn(tag) { 282 | //// case tag { 283 | //// "trainer" -> trainer_decoder 284 | //// "gym-leader" -> gym_leader 285 | //// _ -> decode.fail("PocketMonsterPerson") 286 | //// } 287 | //// }) 288 | //// 289 | //// decoder 290 | //// |> decode.from(data) 291 | //// ``` 292 | 293 | import gleam/dict.{type Dict} 294 | import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError} 295 | import gleam/int 296 | import gleam/list 297 | import gleam/option.{type Option} 298 | import gleam/result 299 | 300 | /// The result that a decoder returns when run. 301 | pub type DecodeResult(t) = 302 | Result(t, List(dynamic.DecodeError)) 303 | 304 | /// A decoder is a value that can be used to turn dynamically typed `Dynamic` 305 | /// data into typed data using the `from` function. 306 | /// 307 | /// Several smaller decoders can be combined to make larger decoders using 308 | /// functions such as `list` and `field`. 309 | /// 310 | pub opaque type Decoder(t) { 311 | Decoder(continuation: fn(Dynamic) -> DecodeResult(t)) 312 | } 313 | 314 | /// Create a new decoder for a given constructor function. If this function is 315 | /// a function that takes parameters one-at-a-time, such as anonymous functions 316 | /// made with `use name <- decode.parameter`, then the decoder can be used with 317 | /// the `decode.field` function to decode a value that contains multiple other 318 | /// values. 319 | /// 320 | /// # Examples 321 | /// 322 | /// ```gleam 323 | /// let data = dynamic.from(dict.from_list([ 324 | /// #("email", "lucy@example.com"), 325 | /// #("name", "Lucy"), 326 | /// ])) 327 | /// 328 | /// decode.into({ 329 | /// use name <- decode.parameter 330 | /// use email <- decode.parameter 331 | /// SignUp(name: name, email: email) 332 | /// }) 333 | /// |> decode.field("name", decode.string) 334 | /// |> decode.field("email", decode.string) 335 | /// |> decode.from(data) 336 | /// // -> Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 337 | /// ``` 338 | /// 339 | pub fn into(constructor: t1) -> Decoder(t1) { 340 | Decoder(continuation: fn(_) { Ok(constructor) }) 341 | } 342 | 343 | /// This function is used to create constructor functions that take arguments 344 | /// one at a time, making them suitable for passing to the `into` function. 345 | /// 346 | /// # Examples 347 | /// 348 | /// ```gleam 349 | /// let data = dynamic.from(dict.from_list([ 350 | /// #("email", "lucy@example.com"), 351 | /// #("name", "Lucy"), 352 | /// ])) 353 | /// 354 | /// decode.into({ 355 | /// use name <- decode.parameter 356 | /// use email <- decode.parameter 357 | /// SignUp(name: name, email: email) 358 | /// }) 359 | /// |> decode.field("name", decode.string) 360 | /// |> decode.field("email", decode.string) 361 | /// |> decode.from(data) 362 | /// // -> Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 363 | /// ``` 364 | /// 365 | pub fn parameter(body: fn(t1) -> t2) -> fn(t1) -> t2 { 366 | body 367 | } 368 | 369 | /// Run a decoder on a `Dynamic` value, decoding the value if it is of the 370 | /// desired type, or returning errors. 371 | /// 372 | /// The second parameter is a field name which will be used to index into the 373 | /// `Dynamic` data. 374 | /// 375 | /// This function will index into dictionaries with any key type, and if the key is 376 | /// an int then it'll also index into Erlang tuples and JavaScript arrays. 377 | /// 378 | /// # Examples 379 | /// 380 | /// ```gleam 381 | /// let data = dynamic.from(dict.from_list([ 382 | /// #("email", "lucy@example.com"), 383 | /// #("name", "Lucy"), 384 | /// ])) 385 | /// 386 | /// decode.into({ 387 | /// use name <- decode.parameter 388 | /// use email <- decode.parameter 389 | /// SignUp(name: name, email: email) 390 | /// }) 391 | /// |> decode.field("name", decode.string) 392 | /// |> decode.field("email", decode.string) 393 | /// |> decode.from(data) 394 | /// // -> Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 395 | /// ``` 396 | /// 397 | /// If you wish to decode a value that is more deeply nested within the dynamic 398 | /// data, see [`subfield`](#subfield) and [`at`](#at). 399 | /// 400 | pub fn field( 401 | decoder: Decoder(fn(t1) -> t2), 402 | field_name: name, 403 | field_decoder: Decoder(t1), 404 | ) -> Decoder(t2) { 405 | subfield(decoder, [field_name], field_decoder) 406 | } 407 | 408 | /// The same as [`field`](#field), except taking a path to the value rather 409 | /// than a field name. 410 | /// 411 | /// # Examples 412 | /// 413 | /// ```gleam 414 | /// let data = dynamic.from(dict.from_list([ 415 | /// #("data", dict.from_list([ 416 | /// #("email", "lucy@example.com"), 417 | /// #("name", "Lucy"), 418 | /// ])) 419 | /// ])) 420 | /// 421 | /// decode.into({ 422 | /// use name <- decode.parameter 423 | /// use email <- decode.parameter 424 | /// SignUp(name: name, email: email) 425 | /// }) 426 | /// |> decode.subfield(["data", "name"], decode.string) 427 | /// |> decode.subfield(["data", "email"], decode.string) 428 | /// |> decode.from(data) 429 | /// // -> Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 430 | /// ``` 431 | /// 432 | pub fn subfield( 433 | decoder: Decoder(fn(t1) -> t2), 434 | field_path: List(name), 435 | field_decoder: Decoder(t1), 436 | ) -> Decoder(t2) { 437 | Decoder(continuation: fn(data) { 438 | let constructor = decoder.continuation(data) 439 | let data = from(at(field_path, field_decoder), data) 440 | case constructor, data { 441 | Ok(constructor), Ok(data) -> Ok(constructor(data)) 442 | Error(e1), Error(e2) -> Error(list.append(e1, e2)) 443 | _, Error(errors) | Error(errors), _ -> Error(errors) 444 | } 445 | }) 446 | } 447 | 448 | /// Run a decoder on a `Dynamic` value, decoding the value if it is of the 449 | /// desired type, or returning errors. 450 | /// 451 | /// # Examples 452 | /// 453 | /// ```gleam 454 | /// decode.into({ 455 | /// use name <- decode.parameter 456 | /// use email <- decode.parameter 457 | /// SignUp(name: name, email: email) 458 | /// }) 459 | /// |> decode.field("email", decode.string) 460 | /// |> decode.field("password", decode.string) 461 | /// |> decode.from(data) 462 | /// ``` 463 | /// 464 | pub fn from(decoder: Decoder(t), data: Dynamic) -> DecodeResult(t) { 465 | decoder.continuation(data) 466 | } 467 | 468 | /// A decoder that decodes `String` values. 469 | /// 470 | /// # Examples 471 | /// 472 | /// ```gleam 473 | /// decode.string 474 | /// |> decode.from(dynamic.from("Hello!")) 475 | /// // -> Ok("Hello!") 476 | /// ``` 477 | /// 478 | pub const string: Decoder(String) = Decoder(continuation: dynamic.string) 479 | 480 | /// A decoder that decodes `Bool` values. 481 | /// 482 | /// # Examples 483 | /// 484 | /// ```gleam 485 | /// decode.bool 486 | /// |> decode.from(dynamic.from(True)) 487 | /// // -> Ok(True) 488 | /// ``` 489 | /// 490 | pub const bool: Decoder(Bool) = Decoder(continuation: dynamic.bool) 491 | 492 | /// A decoder that decodes `Int` values. 493 | /// 494 | /// # Examples 495 | /// 496 | /// ```gleam 497 | /// decode.int 498 | /// |> decode.from(dynamic.from(147)) 499 | /// // -> Ok(147) 500 | /// ``` 501 | /// 502 | pub const int: Decoder(Int) = Decoder(continuation: dynamic.int) 503 | 504 | /// A decoder that decodes `Float` values. 505 | /// 506 | /// # Examples 507 | /// 508 | /// ```gleam 509 | /// decode.float 510 | /// |> decode.from(dynamic.from(3.14)) 511 | /// // -> Ok(3.14) 512 | /// ``` 513 | /// 514 | pub const float: Decoder(Float) = Decoder(continuation: dynamic.float) 515 | 516 | /// A decoder that decodes `Dynamic` values. This decoder never returns an error. 517 | /// 518 | /// # Examples 519 | /// 520 | /// ```gleam 521 | /// decode.dynamic 522 | /// |> decode.from(dynamic.from(3.14)) 523 | /// // -> Ok(dynamic.from(3.14)) 524 | /// ``` 525 | /// 526 | pub const dynamic: Decoder(Dynamic) = Decoder(continuation: Ok) 527 | 528 | /// A decoder that decodes `BitArray` values. This decoder never returns an error. 529 | /// 530 | /// # Examples 531 | /// 532 | /// ```gleam 533 | /// decode.bit_array 534 | /// |> decode.from(dynamic.from(<<5, 7>>)) 535 | /// // -> Ok(<<5, 7>>) 536 | /// ``` 537 | /// 538 | pub const bit_array: Decoder(BitArray) = Decoder( 539 | continuation: dynamic.bit_array, 540 | ) 541 | 542 | /// A decoder that decodes lists where all elements are decoded with a given 543 | /// decoder. 544 | /// 545 | /// # Examples 546 | /// 547 | /// ```gleam 548 | /// decode.list(of: decode.int) 549 | /// |> decode.from(dynamic.from([1, 2, 3])) 550 | /// // -> Ok([1, 2, 3]) 551 | /// ``` 552 | /// 553 | pub fn list(of item: Decoder(a)) -> Decoder(List(a)) { 554 | Decoder(continuation: dynamic.list(item.continuation)) 555 | } 556 | 557 | /// A decoder that decodes dicts where all keys and vales are decoded with 558 | /// given decoders. 559 | /// 560 | /// # Examples 561 | /// 562 | /// ```gleam 563 | /// let values = dict.from_list([ 564 | /// #("one", 1), 565 | /// #("two", 2), 566 | /// ]) 567 | /// decode.dict(decode.string, decode.int) 568 | /// |> decode.from(dynamic.from(values)) 569 | /// // -> Ok(values) 570 | /// ``` 571 | /// 572 | pub fn dict( 573 | key: Decoder(key), 574 | value: Decoder(value), 575 | ) -> Decoder(Dict(key, value)) { 576 | Decoder(continuation: dynamic.dict(key.continuation, value.continuation)) 577 | } 578 | 579 | /// A decoder that decodes nullable values of a type decoded by with a given 580 | /// decoder. 581 | /// 582 | /// This function can handle common representations of null on all runtimes, such as 583 | /// `nil`, `null`, and `undefined` on Erlang, and `undefined` and `null` on 584 | /// JavaScript. 585 | /// 586 | /// # Examples 587 | /// 588 | /// ```gleam 589 | /// decode.optional(decode.int) 590 | /// |> decode.from(dynamic.from(100)) 591 | /// // -> Ok(option.Some(100)) 592 | /// ``` 593 | /// 594 | /// ```gleam 595 | /// decode.optional(decode.int) 596 | /// |> decode.from(dynamic.from(Nil)) 597 | /// // -> Ok(option.None) 598 | /// ``` 599 | /// 600 | pub fn optional(item: Decoder(a)) -> Decoder(Option(a)) { 601 | Decoder(continuation: dynamic.optional(item.continuation)) 602 | } 603 | 604 | /// A decoder that decodes a value that is nested within other values. For 605 | /// example, decoding a value that is within some deeply nested JSON objects. 606 | /// 607 | /// This function will index into dictionaries with any key type, and if the key is 608 | /// an int then it'll also index into Erlang tuples and JavaScript arrays. 609 | /// 610 | /// # Examples 611 | /// 612 | /// ```gleam 613 | /// let data = dynamic.from(dict.from_list([ 614 | /// #("one", dict.from_list([ 615 | /// #("two", 1000), 616 | /// ])), 617 | /// ])) 618 | /// 619 | /// decode.at(["one", "two"], decode.int) 620 | /// |> decode.from(data) 621 | /// // -> Ok(1000) 622 | /// ``` 623 | /// 624 | /// ```gleam 625 | /// decode.optional(decode.int) 626 | /// |> decode.from(dynamic.from(Nil)) 627 | /// // -> Ok(option.None) 628 | /// ``` 629 | /// 630 | pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) { 631 | Decoder(continuation: fn(data) { 632 | let decoder = 633 | list.fold_right(path, inner.continuation, fn(dyn_decoder, segment) { 634 | index(segment, dyn_decoder, _) 635 | }) 636 | decoder(data) 637 | }) 638 | } 639 | 640 | // Indexes into either a tuple/array, or a dict/map/object depending on the key 641 | fn index(key: a, inner: dynamic.Decoder(b), data: Dynamic) -> DecodeResult(b) { 642 | case bare_index(data, key) { 643 | Ok(data) -> 644 | case inner(data) { 645 | Ok(data) -> Ok(data) 646 | Error(errors) -> Error(push_path(errors, key)) 647 | } 648 | Error(kind) -> Error([DecodeError(kind, dynamic.classify(data), [])]) 649 | } 650 | } 651 | 652 | @external(erlang, "decode_ffi", "index") 653 | @external(javascript, "./decode_ffi.mjs", "index") 654 | fn bare_index(data: Dynamic, key: anything) -> Result(Dynamic, String) 655 | 656 | /// Apply a transformation function to any value decoded by the decoder. 657 | /// 658 | /// # Examples 659 | /// 660 | /// ```gleam 661 | /// decode.int 662 | /// |> decode.map(int.to_string) 663 | /// |> decode.from(dynamic.from(1000)) 664 | /// // -> Ok("1000") 665 | /// ``` 666 | /// 667 | pub fn map(decoder: Decoder(a), transformer: fn(a) -> b) -> Decoder(b) { 668 | Decoder(continuation: fn(d) { 669 | case decoder.continuation(d) { 670 | Ok(a) -> Ok(transformer(a)) 671 | Error(e) -> Error(e) 672 | } 673 | }) 674 | } 675 | 676 | /// Apply a transformation function to any errors returned by the decoder. 677 | /// 678 | pub fn map_errors( 679 | decoder: Decoder(a), 680 | transformer: fn(List(DecodeError)) -> List(DecodeError), 681 | ) -> Decoder(a) { 682 | Decoder(continuation: fn(d) { 683 | case decoder.continuation(d) { 684 | Ok(a) -> Ok(a) 685 | Error(e) -> Error(transformer(e)) 686 | } 687 | }) 688 | } 689 | 690 | /// Replace all errors produced by a decoder with one single error for a named 691 | /// expected type. 692 | /// 693 | /// This function may be useful if you wish to simplify errors before 694 | /// presenting them to a user, particularly when using the `one_of` function. 695 | /// 696 | /// # Examples 697 | /// 698 | /// ```gleam 699 | /// decode.string 700 | /// |> decode.collapse_errors("MyThing") 701 | /// |> decode.from(dynamic.from(1000)) 702 | /// // -> Error([DecodeError("MyThing", "Int", [])]) 703 | /// ``` 704 | /// 705 | pub fn collapse_errors(decoder: Decoder(a), name: String) -> Decoder(a) { 706 | Decoder(continuation: fn(d) { 707 | case decoder.continuation(d) { 708 | Ok(a) -> Ok(a) 709 | Error(_) -> Error([DecodeError(name, dynamic.classify(d), [])]) 710 | } 711 | }) 712 | } 713 | 714 | /// Create a new decoder based upon the value of a previous decoder. 715 | /// 716 | /// This may be useful for when you need to know some of the structure of the 717 | /// dynamic value in order to know how to decode the rest of it. 718 | /// 719 | pub fn then(decoder: Decoder(a), next: fn(a) -> Decoder(b)) -> Decoder(b) { 720 | Decoder(continuation: fn(d) { 721 | case decoder.continuation(d) { 722 | Ok(a) -> next(a) |> from(d) 723 | Error(e) -> Error(e) 724 | } 725 | }) 726 | } 727 | 728 | /// Create a new decoder from several other decoders. Each of the inner 729 | /// decoders is run in turn, and the value from the first to succeed is used. 730 | /// 731 | /// If no decoder succeeds then the errors from the final decoder is used. 732 | /// If you wish for different errors then you may wish to use the 733 | /// `collapse_errors` or `map_errors` functions. 734 | /// 735 | /// # Examples 736 | /// 737 | /// ```gleam 738 | /// decode.one_of([ 739 | /// decode.string, 740 | /// decode.int |> decode.map(int.to_string), 741 | /// ]) 742 | /// |> decode.from(dynamic.from(1000)) 743 | /// // -> Ok("1000") 744 | /// ``` 745 | /// 746 | pub fn one_of(decoders: List(Decoder(a))) -> Decoder(a) { 747 | Decoder(continuation: fn(d) { run_decoders(d, decoders) }) 748 | } 749 | 750 | fn run_decoders(data: Dynamic, decoders: List(Decoder(a))) -> DecodeResult(a) { 751 | case decoders { 752 | [] -> Error([DecodeError("nothing", dynamic.classify(data), [])]) 753 | 754 | [decoder] -> from(decoder, data) 755 | 756 | [decoder, ..decoders] -> 757 | case from(decoder, data) { 758 | Ok(value) -> Ok(value) 759 | Error(_) -> run_decoders(data, decoders) 760 | } 761 | } 762 | } 763 | 764 | fn push_path(errors: List(DecodeError), key: t) -> List(DecodeError) { 765 | let key = dynamic.from(key) 766 | let decoder = 767 | dynamic.any([ 768 | dynamic.string, 769 | fn(x) { result.map(dynamic.int(x), int.to_string) }, 770 | ]) 771 | let key = case decoder(key) { 772 | Ok(key) -> key 773 | Error(_) -> "<" <> dynamic.classify(key) <> ">" 774 | } 775 | list.map(errors, fn(error) { DecodeError(..error, path: [key, ..error.path]) }) 776 | } 777 | 778 | /// Define a decoder that always fails. The parameter for this function is the 779 | /// name of the type that has failed to decode. 780 | /// 781 | pub fn fail(expected: String) -> Decoder(a) { 782 | Decoder(continuation: fn(d) { 783 | Error([DecodeError(expected, dynamic.classify(d), [])]) 784 | }) 785 | } 786 | -------------------------------------------------------------------------------- /src/decode/zero.gleam: -------------------------------------------------------------------------------- 1 | //// The `Dynamic` type is used to represent dynamically typed data. That is, data 2 | //// that we don't know the precise type of yet, so we need to introspect the data to 3 | //// see if it is of the desired type before we can use it. Typically data like this 4 | //// would come from user input or from untyped languages such as Erlang or JavaScript. 5 | //// 6 | //// This module provides the `Decoder` type and associated functions, which provides 7 | //// a type-safe and composable way to convert dynamic data into some desired type, 8 | //// or into errors if the data doesn't have the desired structure. 9 | //// 10 | //// The `Decoder` type is generic and has 1 type parameter, which is the type that 11 | //// it attempts to zero. A `Decoder(String)` can be used to decode strings, and a 12 | //// `Decoder(Option(Int))` can be used to decode `Option(Int)`s 13 | //// 14 | //// Decoders work using _runtime reflection_ and the data structures of the target 15 | //// platform. Differences between Erlang and JavaScript data structures may impact 16 | //// your decoders, so it is important to test your decoders on all supported 17 | //// platforms. 18 | //// 19 | //// The decoding technique used by this module was inspired by Juraj Petráš' 20 | //// [Toy](https://github.com/Hackder/toy), Go's `encoding/json`, and Elm's 21 | //// `Json.Decode`. Thank you to them! 22 | //// 23 | //// # Examples 24 | //// 25 | //// Dynamic data may come from various sources and so many different syntaxes could 26 | //// be used to describe or construct them. In these examples a pseudocode 27 | //// syntax is used to describe the data. 28 | //// 29 | //// ## Simple types 30 | //// 31 | //// This module defines decoders for simple data types such as [`string`](#string), 32 | //// [`int`](#int), [`float`](#float), [`bit_array`](#bit_array), and [`bool`](#bool). 33 | //// 34 | //// ```gleam 35 | //// // Data: 36 | //// // "Hello, Joe!" 37 | //// 38 | //// let result = zero.run(data, zero.string) 39 | //// assert result == Ok("Hello, Joe!") 40 | //// ``` 41 | //// 42 | //// ## Lists 43 | //// 44 | //// The [`list`](#list) decoder decodes `List`s. To use it you must construct it by 45 | //// passing in another decoder into the `list` function, which is the decoder that 46 | //// is to be used for the elements of the list, type checking both the list and its 47 | //// elements. 48 | //// 49 | //// ```gleam 50 | //// // Data: 51 | //// // [1, 2, 3, 4] 52 | //// 53 | //// let result = zero.run(data, zero.list(zero.int)) 54 | //// assert result == Ok([1, 2, 3, 4]) 55 | //// ``` 56 | //// 57 | //// On Erlang this decoder can decode from lists, and on JavaScript it can 58 | //// decode from lists as well as JavaScript arrays. 59 | //// 60 | //// ## Options 61 | //// 62 | //// The [`optional`](#optional) decoder is used to decode values that may or may not 63 | //// be present. In other environment these might be called "nullable" values. 64 | //// 65 | //// Like the `list` decoder, the `optional` decoder takes another decoder, 66 | //// which is used to decode the value if it is present. 67 | //// 68 | //// ```gleam 69 | //// // Data: 70 | //// // 12.45 71 | //// 72 | //// let result = zero.run(data, zero.optional(zero.int)) 73 | //// assert result == Ok(option.Some(12.45)) 74 | //// ``` 75 | //// ```gleam 76 | //// // Data: 77 | //// // null 78 | //// 79 | //// let result = zero.run(data, zero.optional(zero.int)) 80 | //// assert result == Ok(option.None) 81 | //// ``` 82 | //// 83 | //// This decoder knows how to handle multiple different runtime representations of 84 | //// absent values, including `Nil`, `None`, `null`, and `undefined`. 85 | //// 86 | //// ## Dicts 87 | //// 88 | //// The [`dict`](#dict) decoder decodes `Dicts` and contains two other decoders, one 89 | //// for the keys, one for the values. 90 | //// 91 | //// ```gleam 92 | //// // Data: 93 | //// // { "Lucy" -> 10, "Nubi" -> 20 } 94 | //// 95 | //// let result = zero.run(data, zero.dict(zero.string, zero.int)) 96 | //// assert result == Ok(dict.from_list([ 97 | //// #("Lucy", 10), 98 | //// #("Nubi", 20), 99 | //// ])) 100 | //// ``` 101 | //// 102 | //// ## Indexing objects 103 | //// 104 | //// The [`at`](#at) decoder can be used to decode a value that is nested within 105 | //// key-value containers such as Gleam dicts, Erlang maps, or JavaScript objects. 106 | //// 107 | //// ```gleam 108 | //// // Data: 109 | //// // { "one" -> { "two" -> 123 } } 110 | //// 111 | //// let result = zero.run(data, zero.at(["one", "two"], zero.int)) 112 | //// assert result == Ok(123) 113 | //// ``` 114 | //// 115 | //// ## Indexing arrays 116 | //// 117 | //// If you use ints as keys then the [`at`](#at) decoder can be used to index into 118 | //// array-like containers such as Gleam or Erlang tuples, or JavaScript arrays. 119 | //// 120 | //// ```gleam 121 | //// // Data: 122 | //// // ["one", "two", "three"] 123 | //// 124 | //// let result = zero.run(data, zero.at([1], zero.string)) 125 | //// assert result == Ok("two") 126 | //// ``` 127 | //// 128 | //// ## Records 129 | //// 130 | //// Decoding records from dynamic data is more complex and requires combining a 131 | //// decoder for each field and a special constructor that builds your records with 132 | //// the decoded field values. 133 | //// 134 | //// ```gleam 135 | //// // Data: 136 | //// // { 137 | //// // "score" -> 180, 138 | //// // "name" -> "Mel Smith", 139 | //// // "is-admin" -> false, 140 | //// // "enrolled" -> true, 141 | //// // "colour" -> "Red", 142 | //// // } 143 | //// 144 | //// let decoder = { 145 | //// use name <- zero.field("name", zero.string) 146 | //// use score <- zero.field("score", zero.int) 147 | //// use colour <- zero.field("colour", zero.string) 148 | //// use enrolled <- zero.field("enrolled", zero.bool) 149 | //// zero.success(Player(name:, score:, colour:, enrolled:)) 150 | //// } 151 | //// 152 | //// let result = zero.run(data, decoder) 153 | //// assert result == Ok(Player("Mel Smith", 180, "Red", True)) 154 | //// ``` 155 | //// 156 | //// ## Enum variants 157 | //// 158 | //// Imagine you have a custom type where all the variants do not contain any values. 159 | //// 160 | //// ```gleam 161 | //// pub type PocketMonsterType { 162 | //// Fire 163 | //// Water 164 | //// Grass 165 | //// Electric 166 | //// } 167 | //// ``` 168 | //// 169 | //// You might choose to encode these variants as strings, `"fire"` for `Fire`, 170 | //// `"water"` for `Water`, and so on. To decode them you'll need to decode the dynamic 171 | //// data as a string, but then you'll need to decode it further still as not all 172 | //// strings are valid values for the enum. This can be done with the `then` 173 | //// function, which enables running a second decoder after the first one 174 | //// succeeds. 175 | //// 176 | //// ```gleam 177 | //// let decoder = { 178 | //// use decoded_string <- zero.then(zero.string) 179 | //// case decoded_string { 180 | //// // Return succeeding decoders for valid strings 181 | //// "fire" -> zero.success(Fire) 182 | //// "water" -> zero.success(Water) 183 | //// "grass" -> zero.success(Grass) 184 | //// "electric" -> zero.success(Electric) 185 | //// // Return a failing decoder for any other strings 186 | //// _ -> zero.failure(Fire, "PocketMonsterType") 187 | //// } 188 | //// } 189 | //// 190 | //// let result = zero.run(dynamic.from("water"), decoder) 191 | //// assert result == Ok(Water) 192 | //// 193 | //// let result = zero.run(dynamic.from("wobble"), decoder) 194 | //// assert result == Error([DecodeError("PocketMonsterType", "String", [])]) 195 | //// ``` 196 | //// 197 | //// ## Record variants 198 | //// 199 | //// Decoding type variants that contain other values is done by combining the 200 | //// techniques from the "enum variants" and "records" examples. Imagine you have 201 | //// this custom type that you want to decode: 202 | //// 203 | //// ```gleam 204 | //// pub type PocketMonsterPerson { 205 | //// Trainer(name: String, badge_count: Int) 206 | //// GymLeader(name: String, speciality: PocketMonsterType) 207 | //// } 208 | //// ``` 209 | //// And you would like to be able to decode these from dynamic data like this: 210 | //// ```erlang 211 | //// { 212 | //// "type" -> "trainer", 213 | //// "name" -> "Ash", 214 | //// "badge-count" -> 1, 215 | //// } 216 | //// ``` 217 | //// ```erlang 218 | //// { 219 | //// "type" -> "gym-leader", 220 | //// "name" -> "Misty", 221 | //// "speciality" -> "water", 222 | //// } 223 | //// ``` 224 | //// 225 | //// Notice how both documents have a `"type"` field, which is used to indicate which 226 | //// variant the data is for. 227 | //// 228 | //// First, define decoders for each of the variants: 229 | //// 230 | //// ```gleam 231 | //// let trainer_decoder = { 232 | //// use name <- zero.field("name", zero.string) 233 | //// use badge_count <- zero.field("badge-count", zero.int) 234 | //// zero.success(Trainer(name, badge_count)) 235 | //// }) 236 | //// 237 | //// let gym_leader_decoder = { 238 | //// use name <- zero.field("name", zero.string) 239 | //// use speciality <- zero.field("speciality", pocket_monster_type_decoder) 240 | //// zero.success(GymLeader(name, speciality)) 241 | //// } 242 | //// ``` 243 | //// 244 | //// A third decoder can be used to extract and decode the `"type"` field, and the 245 | //// `then` function then returns whichever decoder is suitable for the document. 246 | //// 247 | //// ```gleam 248 | //// let decoder = { 249 | //// use tag <- zero.field("type", zero.string) 250 | //// case tag { 251 | //// "gym-leader" -> gym_leader_decoder 252 | //// _ -> trainer_decoder 253 | //// } 254 | //// } 255 | //// 256 | //// zero.run(data, decoder) 257 | //// ``` 258 | 259 | import gleam/dict.{type Dict} 260 | import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError} 261 | import gleam/int 262 | import gleam/list 263 | import gleam/option.{type Option, None, Some} 264 | import gleam/result 265 | 266 | /// A decoder is a value that can be used to turn dynamically typed `Dynamic` 267 | /// data into typed data using the `run` function. 268 | /// 269 | /// Several smaller decoders can be combined to make larger decoders using 270 | /// functions such as `list` and `field`. 271 | /// 272 | pub opaque type Decoder(t) { 273 | Decoder(function: fn(Dynamic) -> #(t, List(dynamic.DecodeError))) 274 | } 275 | 276 | /// The same as [`field`](#field), except taking a path to the value rather 277 | /// than a field name. 278 | /// 279 | /// This function will index into dictionaries with any key type, and if the key is 280 | /// an int then it'll also index into Erlang tuples and JavaScript arrays, and 281 | /// the first two elements of Gleam lists. 282 | /// 283 | /// # Examples 284 | /// 285 | /// ```gleam 286 | /// let data = dynamic.from(dict.from_list([ 287 | /// #("data", dict.from_list([ 288 | /// #("email", "lucy@example.com"), 289 | /// #("name", "Lucy"), 290 | /// ])) 291 | /// ])) 292 | /// 293 | /// let decoder = { 294 | /// use name <- zero.subfield(["data", "name"], zero.string) 295 | /// use email <- zero.subfield(["data", "email"], zero.string) 296 | /// zero.success(SignUp(name: name, email: email)) 297 | /// } 298 | /// let result = zero.run(data, decoder) 299 | /// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 300 | /// ``` 301 | /// 302 | pub fn subfield( 303 | field_path: List(name), 304 | field_decoder: Decoder(t), 305 | next: fn(t) -> Decoder(final), 306 | ) -> Decoder(final) { 307 | Decoder(function: fn(data) { 308 | let #(out, errors1) = 309 | index(field_path, [], field_decoder.function, data, fn(data, position) { 310 | let #(default, _) = field_decoder.function(data) 311 | #(default, [DecodeError("Field", "Nothing", [])]) 312 | |> push_path(list.reverse(position)) 313 | }) 314 | let #(out, errors2) = next(out).function(data) 315 | #(out, list.append(errors1, errors2)) 316 | }) 317 | } 318 | 319 | /// Run a decoder on a `Dynamic` value, decoding the value if it is of the 320 | /// desired type, or returning errors. 321 | /// 322 | /// # Examples 323 | /// 324 | /// ```gleam 325 | /// let decoder = { 326 | /// use name <- zero.field("email", zero.string) 327 | /// use email <- zero.field("password", zero.string) 328 | /// zero.success(SignUp(name: name, email: email)) 329 | /// } 330 | /// 331 | /// zero.run(data, decoder) 332 | /// ``` 333 | /// 334 | pub fn run( 335 | data: Dynamic, 336 | decoder: Decoder(t), 337 | ) -> Result(t, List(dynamic.DecodeError)) { 338 | let #(maybe_invalid_data, errors) = decoder.function(data) 339 | case errors { 340 | [] -> Ok(maybe_invalid_data) 341 | _ -> Error(errors) 342 | } 343 | } 344 | 345 | /// A decoder that decodes a value that is nested within other values. For 346 | /// example, decoding a value that is within some deeply nested JSON objects. 347 | /// 348 | /// This function will index into dictionaries with any key type, and if the key is 349 | /// an int then it'll also index into Erlang tuples and JavaScript arrays, and 350 | /// the first two elements of Gleam lists. 351 | /// 352 | /// # Examples 353 | /// 354 | /// ```gleam 355 | /// let decoder = zero.at(["one", "two"], zero.int) 356 | /// 357 | /// let data = dynamic.from(dict.from_list([ 358 | /// #("one", dict.from_list([ 359 | /// #("two", 1000), 360 | /// ])), 361 | /// ])) 362 | /// 363 | /// 364 | /// zero.run(data, decoder) 365 | /// // -> Ok(1000) 366 | /// ``` 367 | /// 368 | /// ```gleam 369 | /// dynamic.from(Nil) 370 | /// |> zero.run(zero.optional(zero.int)) 371 | /// // -> Ok(option.None) 372 | /// ``` 373 | /// 374 | pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) { 375 | Decoder(function: fn(data) { 376 | index(path, [], inner.function, data, fn(data, position) { 377 | let #(default, _) = inner.function(data) 378 | #(default, [DecodeError("Field", "Nothing", [])]) 379 | |> push_path(list.reverse(position)) 380 | }) 381 | }) 382 | } 383 | 384 | fn index( 385 | path: List(a), 386 | position: List(a), 387 | inner: fn(Dynamic) -> #(b, List(dynamic.DecodeError)), 388 | data: Dynamic, 389 | handle_miss: fn(Dynamic, List(a)) -> #(b, List(dynamic.DecodeError)), 390 | ) -> #(b, List(dynamic.DecodeError)) { 391 | case path { 392 | [] -> { 393 | inner(data) 394 | |> push_path(list.reverse(position)) 395 | } 396 | 397 | [key, ..path] -> { 398 | case bare_index(data, key) { 399 | Ok(Some(data)) -> { 400 | index(path, [key, ..position], inner, data, handle_miss) 401 | } 402 | Ok(None) -> { 403 | handle_miss(data, [key, ..position]) 404 | } 405 | Error(kind) -> { 406 | let #(default, _) = inner(data) 407 | #(default, [DecodeError(kind, dynamic.classify(data), [])]) 408 | |> push_path(list.reverse(position)) 409 | } 410 | } 411 | } 412 | } 413 | } 414 | 415 | @external(erlang, "decode_ffi", "strict_index") 416 | @external(javascript, "../decode_ffi.mjs", "strict_index") 417 | fn bare_index(data: Dynamic, key: anything) -> Result(Option(Dynamic), String) 418 | 419 | fn push_path( 420 | layer: #(t, List(DecodeError)), 421 | path: List(key), 422 | ) -> #(t, List(DecodeError)) { 423 | let decoder = 424 | dynamic.any([ 425 | dynamic.string, 426 | fn(x) { result.map(dynamic.int(x), int.to_string) }, 427 | ]) 428 | let path = 429 | list.map(path, fn(key) { 430 | let key = dynamic.from(key) 431 | case decoder(key) { 432 | Ok(key) -> key 433 | Error(_) -> "<" <> dynamic.classify(key) <> ">" 434 | } 435 | }) 436 | let errors = 437 | list.map(layer.1, fn(error) { 438 | DecodeError(..error, path: list.append(path, error.path)) 439 | }) 440 | #(layer.0, errors) 441 | } 442 | 443 | /// Finalise a decoder having successfully extracted a value. 444 | /// 445 | /// # Examples 446 | /// 447 | /// ```gleam 448 | /// let data = dynamic.from(dict.from_list([ 449 | /// #("email", "lucy@example.com"), 450 | /// #("name", "Lucy"), 451 | /// ])) 452 | /// 453 | /// let decoder = { 454 | /// use name <- zero.field("name", string) 455 | /// use email <- zero.field("email", string) 456 | /// zero.success(SignUp(name: name, email: email)) 457 | /// } 458 | /// 459 | /// let result = zero.run(data, decoder) 460 | /// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 461 | /// ``` 462 | /// 463 | pub fn success(data: t) -> Decoder(t) { 464 | Decoder(function: fn(_) { #(data, []) }) 465 | } 466 | 467 | /// Construct a decode error for some unexpected dynamic data. 468 | /// 469 | pub fn decode_error( 470 | expected expected: String, 471 | found found: Dynamic, 472 | ) -> List(dynamic.DecodeError) { 473 | [DecodeError(expected:, found: dynamic.classify(found), path: [])] 474 | } 475 | 476 | /// Run a decoder on a field of a `Dynamic` value, decoding the value if it is 477 | /// of the desired type, or returning errors. An error is returned if there is 478 | /// no field for the specified key. 479 | /// 480 | /// This function will index into dictionaries with any key type, and if the key is 481 | /// an int then it'll also index into Erlang tuples and JavaScript arrays, and 482 | /// the first two elements of Gleam lists. 483 | /// 484 | /// # Examples 485 | /// 486 | /// ```gleam 487 | /// let data = dynamic.from(dict.from_list([ 488 | /// #("email", "lucy@example.com"), 489 | /// #("name", "Lucy"), 490 | /// ])) 491 | /// 492 | /// let decoder = { 493 | /// use name <- zero.field("name", string) 494 | /// use email <- zero.field("email", string) 495 | /// SignUp(name: name, email: email) 496 | /// } 497 | /// 498 | /// let result = zero.run(data, decoder) 499 | /// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) 500 | /// ``` 501 | /// 502 | /// If you wish to decode a value that is more deeply nested within the dynamic 503 | /// data, see [`subfield`](#subfield) and [`at`](#at). 504 | /// 505 | /// If you wish to return a default in the event that a field is not present, 506 | /// see [`optional_field`](#optional_field) and / [`optionally_at`](#optionally_at). 507 | /// 508 | pub fn field( 509 | field_name: name, 510 | field_decoder: Decoder(t), 511 | next: fn(t) -> Decoder(final), 512 | ) -> Decoder(final) { 513 | subfield([field_name], field_decoder, next) 514 | } 515 | 516 | /// Run a decoder on a field of a `Dynamic` value, decoding the value if it is 517 | /// of the desired type, or returning errors. The given default value is 518 | /// returned if there is no field for the specified key. 519 | /// 520 | /// This function will index into dictionaries with any key type, and if the key is 521 | /// an int then it'll also index into Erlang tuples and JavaScript arrays, and 522 | /// the first two elements of Gleam lists. 523 | /// 524 | /// # Examples 525 | /// 526 | /// ```gleam 527 | /// let data = dynamic.from(dict.from_list([ 528 | /// #("name", "Lucy"), 529 | /// ])) 530 | /// 531 | /// let decoder = { 532 | /// use name <- zero.field("name", string) 533 | /// use email <- zero.optional_field("email", "n/a", string) 534 | /// SignUp(name: name, email: email) 535 | /// } 536 | /// 537 | /// let result = zero.run(data, decoder) 538 | /// assert result == Ok(SignUp(name: "Lucy", email: "n/a")) 539 | /// ``` 540 | /// 541 | pub fn optional_field( 542 | key: name, 543 | default: t, 544 | field_decoder: Decoder(t), 545 | next: fn(t) -> Decoder(final), 546 | ) -> Decoder(final) { 547 | Decoder(function: fn(data) { 548 | let #(out, errors1) = case bare_index(data, key) { 549 | Ok(Some(data)) -> field_decoder.function(data) 550 | Ok(None) -> #(default, []) 551 | Error(kind) -> { 552 | #(default, [DecodeError(kind, dynamic.classify(data), [])]) 553 | |> push_path([key]) 554 | } 555 | } 556 | let #(out, errors2) = next(out).function(data) 557 | #(out, list.append(errors1, errors2)) 558 | }) 559 | } 560 | 561 | /// A decoder that decodes a value that is nested within other values. For 562 | /// example, decoding a value that is within some deeply nested JSON objects. 563 | /// 564 | /// This function will index into dictionaries with any key type, and if the key is 565 | /// an int then it'll also index into Erlang tuples and JavaScript arrays, and 566 | /// the first two elements of Gleam lists. 567 | /// 568 | /// # Examples 569 | /// 570 | /// ```gleam 571 | /// let decoder = zero.optionally_at(["one", "two"], 100, zero.int) 572 | /// 573 | /// let data = dynamic.from(dict.from_list([ 574 | /// #("one", dict.from_list([])), 575 | /// ])) 576 | /// 577 | /// 578 | /// zero.run(data, decoder) 579 | /// // -> Ok(100) 580 | /// ``` 581 | /// 582 | pub fn optionally_at( 583 | path: List(segment), 584 | default: a, 585 | inner: Decoder(a), 586 | ) -> Decoder(a) { 587 | Decoder(function: fn(data) { 588 | index(path, [], inner.function, data, fn(_, _) { #(default, []) }) 589 | }) 590 | } 591 | 592 | fn run_dynamic_function( 593 | data: Dynamic, 594 | zero: t, 595 | f: dynamic.Decoder(t), 596 | ) -> #(t, List(dynamic.DecodeError)) { 597 | case f(data) { 598 | Ok(data) -> #(data, []) 599 | Error(errors) -> #(zero, errors) 600 | } 601 | } 602 | 603 | /// A decoder that decodes `String` values. 604 | /// 605 | /// # Examples 606 | /// 607 | /// ```gleam 608 | /// let result = zero.run(dynamic.from("Hello!"), zero.string) 609 | /// assert result == Ok("Hello!") 610 | /// ``` 611 | /// 612 | pub const string: Decoder(String) = Decoder(decode_string) 613 | 614 | fn decode_string(data: Dynamic) -> #(String, List(dynamic.DecodeError)) { 615 | run_dynamic_function(data, "", dynamic.string) 616 | } 617 | 618 | /// A decoder that decodes `Bool` values. 619 | /// 620 | /// # Examples 621 | /// 622 | /// ```gleam 623 | /// let result = zero.run(dynamic.from(True), zero.bool) 624 | /// assert result == Ok(True) 625 | /// ``` 626 | /// 627 | pub const bool: Decoder(Bool) = Decoder(decode_bool) 628 | 629 | fn decode_bool(data: Dynamic) -> #(Bool, List(dynamic.DecodeError)) { 630 | run_dynamic_function(data, False, dynamic.bool) 631 | } 632 | 633 | /// A decoder that decodes `Int` values. 634 | /// 635 | /// # Examples 636 | /// 637 | /// ```gleam 638 | /// let result = zero.run(dynamic.from(147), zero.int) 639 | /// assert result == Ok(147) 640 | /// ``` 641 | /// 642 | pub const int: Decoder(Int) = Decoder(decode_int) 643 | 644 | fn decode_int(data: Dynamic) -> #(Int, List(dynamic.DecodeError)) { 645 | run_dynamic_function(data, 0, dynamic.int) 646 | } 647 | 648 | /// A decoder that decodes `Float` values. 649 | /// 650 | /// # Examples 651 | /// 652 | /// ```gleam 653 | /// let result = zero.run(dynamic.from(3.14), zero.float) 654 | /// assert result == Ok(3.14) 655 | /// ``` 656 | /// 657 | pub const float: Decoder(Float) = Decoder(decode_float) 658 | 659 | fn decode_float(data: Dynamic) -> #(Float, List(dynamic.DecodeError)) { 660 | run_dynamic_function(data, 0.0, dynamic.float) 661 | } 662 | 663 | /// A decoder that decodes `Dynamic` values. This decoder never returns an error. 664 | /// 665 | /// # Examples 666 | /// 667 | /// ```gleam 668 | /// let result = zero.run(dynamic.from(3.14), zero.dynamic) 669 | /// assert result == Ok(dynamic.from(3.14)) 670 | /// ``` 671 | /// 672 | pub const dynamic: Decoder(Dynamic) = Decoder(decode_dynamic) 673 | 674 | fn decode_dynamic(data: Dynamic) -> #(Dynamic, List(dynamic.DecodeError)) { 675 | #(data, []) 676 | } 677 | 678 | /// A decoder that decodes `BitArray` values. This decoder never returns an error. 679 | /// 680 | /// # Examples 681 | /// 682 | /// ```gleam 683 | /// let result = zero.run(dynamic.from(<<5, 7>>), zero.bit_array) 684 | /// assert result == Ok(<<5, 7>>) 685 | /// ``` 686 | /// 687 | pub const bit_array: Decoder(BitArray) = Decoder(decode_bit_array) 688 | 689 | fn decode_bit_array(data: Dynamic) -> #(BitArray, List(dynamic.DecodeError)) { 690 | run_dynamic_function(data, <<>>, dynamic.bit_array) 691 | } 692 | 693 | /// A decoder that decodes lists where all elements are decoded with a given 694 | /// decoder. 695 | /// 696 | /// # Examples 697 | /// 698 | /// ```gleam 699 | /// let result = 700 | /// zero.run(dynamic.from([1, 2, 3]), zero.list(of: zero.int)) 701 | /// assert result == Ok([1, 2, 3]) 702 | /// ``` 703 | /// 704 | pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) { 705 | Decoder(fn(data) { 706 | decode_list(data, inner.function, fn(p, k) { push_path(p, [k]) }, 0, []) 707 | }) 708 | } 709 | 710 | @external(erlang, "decode_ffi", "list") 711 | @external(javascript, "../decode_ffi.mjs", "list") 712 | fn decode_list( 713 | data: Dynamic, 714 | item: fn(Dynamic) -> #(t, List(dynamic.DecodeError)), 715 | push_path: fn(#(t, List(DecodeError)), key) -> #(t, List(DecodeError)), 716 | index: Int, 717 | acc: List(t), 718 | ) -> #(List(t), List(dynamic.DecodeError)) 719 | 720 | /// A decoder that decodes dicts where all keys and vales are decoded with 721 | /// given decoders. 722 | /// 723 | /// # Examples 724 | /// 725 | /// ```gleam 726 | /// let values = dict.from_list([ 727 | /// #("one", 1), 728 | /// #("two", 2), 729 | /// ]) 730 | /// 731 | /// let result = 732 | /// zero.run(dynamic.from(values), zero.dict(zero.string, zero.int)) 733 | /// assert result == Ok(values) 734 | /// ``` 735 | /// 736 | pub fn dict( 737 | key: Decoder(key), 738 | value: Decoder(value), 739 | ) -> Decoder(Dict(key, value)) { 740 | Decoder(fn(data) { 741 | case decode_dict(data) { 742 | Error(_) -> #(dict.new(), decode_error("Dict", data)) 743 | Ok(dict) -> 744 | dict.fold(dict, #(dict.new(), []), fn(a, k, v) { 745 | // If there are any errors from previous key-value pairs then we 746 | // don't need to run the decoders, instead return the existing acc. 747 | case a.1 { 748 | [] -> fold_dict(a, k, v, key.function, value.function) 749 | _ -> a 750 | } 751 | }) 752 | } 753 | }) 754 | } 755 | 756 | fn fold_dict( 757 | acc: #(Dict(k, v), List(dynamic.DecodeError)), 758 | key: Dynamic, 759 | value: Dynamic, 760 | key_decoder: fn(Dynamic) -> #(k, List(dynamic.DecodeError)), 761 | value_decoder: fn(Dynamic) -> #(v, List(dynamic.DecodeError)), 762 | ) -> #(Dict(k, v), List(dynamic.DecodeError)) { 763 | // First we decode the key. 764 | case key_decoder(key) { 765 | #(key, []) -> 766 | // Then we decode the value. 767 | case value_decoder(value) { 768 | #(value, []) -> { 769 | // It worked! Insert the new key-value pair so we can move onto the next. 770 | let dict = dict.insert(acc.0, key, value) 771 | #(dict, acc.1) 772 | } 773 | #(_, errors) -> push_path(#(dict.new(), errors), ["values"]) 774 | } 775 | #(_, errors) -> push_path(#(dict.new(), errors), ["keys"]) 776 | } 777 | } 778 | 779 | @external(erlang, "decode_ffi", "dict") 780 | @external(javascript, "../decode_ffi.mjs", "dict") 781 | fn decode_dict(data: Dynamic) -> Result(Dict(Dynamic, Dynamic), Nil) 782 | 783 | /// A decoder that decodes nullable values of a type decoded by with a given 784 | /// decoder. 785 | /// 786 | /// This function can handle common representations of null on all runtimes, such as 787 | /// `nil`, `null`, and `undefined` on Erlang, and `undefined` and `null` on 788 | /// JavaScript. 789 | /// 790 | /// # Examples 791 | /// 792 | /// ```gleam 793 | /// let result = zero.run(dynamic.from(100), zero.optional(zero.int)) 794 | /// assert result == Ok(option.Some(100)) 795 | /// ``` 796 | /// 797 | /// ```gleam 798 | /// let result = zero.run(dynamic.from(Nil), zero.optional(zero.int)) 799 | /// assert result == Ok(option.None) 800 | /// ``` 801 | /// 802 | pub fn optional(inner: Decoder(a)) -> Decoder(Option(a)) { 803 | Decoder(function: fn(data) { 804 | case dynamic.optional(Ok)(data) { 805 | Ok(option.None) -> #(option.None, []) 806 | Ok(option.Some(data)) -> { 807 | let #(data, errors) = inner.function(data) 808 | #(option.Some(data), errors) 809 | } 810 | Error(_) -> { 811 | let #(data, errors) = inner.function(data) 812 | #(option.Some(data), errors) 813 | } 814 | } 815 | }) 816 | } 817 | 818 | /// Apply a transformation function to any value decoded by the decoder. 819 | /// 820 | /// # Examples 821 | /// 822 | /// ```gleam 823 | /// let decoder = zero.int |> zero.map(int.to_string) 824 | /// let result = zero.run(dynamic.from(1000), decoder) 825 | /// assert result == Ok("1000") 826 | /// ``` 827 | /// 828 | pub fn map(decoder: Decoder(a), transformer: fn(a) -> b) -> Decoder(b) { 829 | Decoder(function: fn(d) { 830 | let #(data, errors) = decoder.function(d) 831 | #(transformer(data), errors) 832 | }) 833 | } 834 | 835 | /// Apply a transformation function to any errors returned by the decoder. 836 | /// 837 | pub fn map_errors( 838 | decoder: Decoder(a), 839 | transformer: fn(List(DecodeError)) -> List(DecodeError), 840 | ) -> Decoder(a) { 841 | Decoder(function: fn(d) { 842 | let #(data, errors) = decoder.function(d) 843 | #(data, transformer(errors)) 844 | }) 845 | } 846 | 847 | /// Replace all errors produced by a decoder with one single error for a named 848 | /// expected type. 849 | /// 850 | /// This function may be useful if you wish to simplify errors before 851 | /// presenting them to a user, particularly when using the `one_of` function. 852 | /// 853 | /// # Examples 854 | /// 855 | /// ```gleam 856 | /// let decoder = zero.string |> zero.collapse_errors("MyThing") 857 | /// let result = zero.run(dynamic.from(1000), decoder) 858 | /// assert result == Error([DecodeError("MyThing", "Int", [])]) 859 | /// ``` 860 | /// 861 | pub fn collapse_errors(decoder: Decoder(a), name: String) -> Decoder(a) { 862 | Decoder(function: fn(dynamic_data) { 863 | let #(data, errors) as layer = decoder.function(dynamic_data) 864 | case errors { 865 | [] -> layer 866 | _ -> #(data, decode_error(name, dynamic_data)) 867 | } 868 | }) 869 | } 870 | 871 | /// Create a new decoder based upon the value of a previous decoder. 872 | /// 873 | /// This may be useful to run one previous decoder to use in further decoding. 874 | /// 875 | pub fn then(decoder: Decoder(a), next: fn(a) -> Decoder(b)) -> Decoder(b) { 876 | Decoder(function: fn(dynamic_data) { 877 | let #(data, errors) = decoder.function(dynamic_data) 878 | let decoder = next(data) 879 | let #(data, _) as layer = decoder.function(dynamic_data) 880 | case errors { 881 | [] -> layer 882 | _ -> #(data, errors) 883 | } 884 | }) 885 | } 886 | 887 | /// Create a new decoder from several other decoders. Each of the inner 888 | /// decoders is run in turn, and the value from the first to succeed is used. 889 | /// 890 | /// If no decoder succeeds then the errors from the first decoder is used. 891 | /// If you wish for different errors then you may wish to use the 892 | /// `collapse_errors` or `map_errors` functions. 893 | /// 894 | /// # Examples 895 | /// 896 | /// ```gleam 897 | /// let decoder = zero.one_of(zero.string, or: [ 898 | /// zero.int |> zero.map(int.to_string), 899 | /// zero.float |> zero.map(float.to_string), 900 | /// ]) 901 | /// zero.run(dynamic.from(1000), decoder) 902 | /// // -> Ok("1000") 903 | /// ``` 904 | /// 905 | pub fn one_of( 906 | first: Decoder(a), 907 | or alternatives: List(Decoder(a)), 908 | ) -> Decoder(a) { 909 | Decoder(function: fn(dynamic_data) { 910 | let #(_, errors) as layer = first.function(dynamic_data) 911 | case errors { 912 | [] -> layer 913 | _ -> run_decoders(dynamic_data, layer, alternatives) 914 | } 915 | }) 916 | } 917 | 918 | fn run_decoders( 919 | data: Dynamic, 920 | failure: #(a, List(DecodeError)), 921 | decoders: List(Decoder(a)), 922 | ) -> #(a, List(DecodeError)) { 923 | case decoders { 924 | [] -> failure 925 | 926 | [decoder, ..decoders] -> { 927 | let #(_, errors) as layer = decoder.function(data) 928 | case errors { 929 | [] -> layer 930 | _ -> run_decoders(data, failure, decoders) 931 | } 932 | } 933 | } 934 | } 935 | 936 | /// Define a decoder that always fails. The parameter for this function is the 937 | /// name of the type that has failed to zero. 938 | /// 939 | pub fn failure(zero: a, expected: String) -> Decoder(a) { 940 | Decoder(function: fn(d) { #(zero, decode_error(expected, d)) }) 941 | } 942 | 943 | /// Create a decoder for a new data type from a decoding function. 944 | /// 945 | /// This function is used for new primitive types. For example, you might 946 | /// define a decoder for Erlang's pid type. 947 | /// 948 | /// A default "zero" value is also required to make a decoder. When this 949 | /// decoder is used as part of a larger decoder this zero value used as 950 | /// a placeholder so that the rest of the decoder can continue to run and 951 | /// collect all decoding errors. 952 | /// 953 | /// If you were to make a decoder for the `String` type (rather than using the 954 | /// build-in `string` decoder) you would define it like so: 955 | /// 956 | /// ```gleam 957 | /// import gleam/dynamic 958 | /// import decode/zero 959 | /// 960 | /// pub fn string_decoder() -> zero.Decoder(String) { 961 | /// zero.new_primitive_decoder(dynamic.string, "") 962 | /// } 963 | /// ``` 964 | /// 965 | pub fn new_primitive_decoder( 966 | decoding_function: fn(Dynamic) -> Result(t, List(DecodeError)), 967 | zero: t, 968 | ) -> Decoder(t) { 969 | Decoder(function: fn(d) { 970 | case decoding_function(d) { 971 | Ok(t) -> #(t, []) 972 | Error(errors) -> #(zero, errors) 973 | } 974 | }) 975 | } 976 | 977 | /// Create a decoder that will lazily create the wrapped decoder when needed. 978 | /// 979 | /// This function can be used to create decoders that use themselves, for 980 | /// example, for deeply nested data. 981 | /// 982 | /// ```gleam 983 | /// import gleam/dynamic 984 | /// import decode/zero.{type Decoder} 985 | /// 986 | /// type Nested { 987 | /// Nested(List(Nested)) 988 | /// Value(String) 989 | /// } 990 | /// 991 | /// fn nested_decoder() -> Decoder(Nested) { 992 | /// use <- zero.recursive 993 | /// zero.one_of(zero.string |> zero.map(Value), [ 994 | /// zero.list(nested_decoder()) |> zero.map(Nested), 995 | /// ]) 996 | /// } 997 | /// ``` 998 | /// 999 | pub fn recursive(inner: fn() -> Decoder(a)) -> Decoder(a) { 1000 | Decoder(function: fn(data) { 1001 | let decoder = inner() 1002 | decoder.function(data) 1003 | }) 1004 | } 1005 | -------------------------------------------------------------------------------- /src/decode_ffi.erl: -------------------------------------------------------------------------------- 1 | -module(decode_ffi). 2 | 3 | -export([strict_index/2, index/2, list/5, dict/1]). 4 | 5 | strict_index([X | _], 0) -> 6 | {ok, {some, X}}; 7 | strict_index([_, X | _], 1) -> 8 | {ok, {some, X}}; 9 | strict_index(Tuple, Index) when is_tuple(Tuple) andalso is_integer(Index) -> 10 | {ok, try 11 | {some, element(Index + 1, Tuple)} 12 | catch _:_ -> 13 | none 14 | end}; 15 | strict_index(Map, Key) when is_map(Map) -> 16 | {ok, try 17 | {some, maps:get(Key, Map)} 18 | catch _:_ -> 19 | none 20 | end}; 21 | strict_index(_, Index) when is_integer(Index) -> 22 | {error, <<"Indexable">>}; 23 | strict_index(_, _) -> 24 | {error, <<"Dict">>}. 25 | 26 | index([X | _], 0) -> 27 | {ok, X}; 28 | index([_, X | _], 1) -> 29 | {ok, X}; 30 | index(Tuple, Index) when is_tuple(Tuple) andalso is_integer(Index) -> 31 | try 32 | {ok, element(Index + 1, Tuple)} 33 | catch _:_ -> 34 | {ok, nil} 35 | end; 36 | index(Map, Key) when is_map(Map) -> 37 | try 38 | {ok, maps:get(Key, Map)} 39 | catch _:_ -> 40 | {ok, nil} 41 | end; 42 | index(_, Index) when is_integer(Index) -> 43 | {error, <<"Indexable">>}; 44 | index(_, _) -> 45 | {error, <<"Dict">>}. 46 | 47 | list(T, A, B, C, D) when is_tuple(T) -> 48 | list(tuple_to_list(T), A, B, C, D); 49 | list([], _, _, _, Acc) -> 50 | {lists:reverse(Acc), []}; 51 | list([X | Xs], Decode, PushPath, Index, Acc) -> 52 | {Out, Errors} = Decode(X), 53 | case Errors of 54 | [] -> list(Xs, Decode, PushPath, Index + 1, [Out | Acc]); 55 | _ -> PushPath({[], Errors}, integer_to_binary(Index)) 56 | end; 57 | list(Unexpected, _, _, _, []) -> 58 | Found = gleam@dynamic:classify(Unexpected), 59 | Error = {decode_error, <<"List"/utf8>>, Found, []}, 60 | {[], [Error]}; 61 | list(_, _, _, _, Acc) -> 62 | {lists:reverse(Acc), []}. 63 | 64 | dict(#{} = Data) -> {ok, Data}; 65 | dict(_) -> {error, nil}. 66 | -------------------------------------------------------------------------------- /src/decode_ffi.mjs: -------------------------------------------------------------------------------- 1 | import { Ok, Error, List, NonEmpty } from "./gleam.mjs"; 2 | import { default as Dict } from "../gleam_stdlib/dict.mjs"; 3 | import { Some, None } from "../gleam_stdlib/gleam/option.mjs"; 4 | import { DecodeError, classify } from "../gleam_stdlib/gleam/dynamic.mjs"; 5 | 6 | export function strict_index(data, key) { 7 | const int = Number.isInteger(key); 8 | 9 | // Dictionaries and dictionary-like objects can be indexed 10 | if (data instanceof Dict || data instanceof WeakMap || data instanceof Map) { 11 | const token = {}; 12 | const entry = data.get(key, token); 13 | if (entry === token) return new Ok(new None()); 14 | return new Ok(new Some(entry)); 15 | } 16 | 17 | // The first 3 elements of lists can be indexed 18 | if ((key === 0 || key === 1 || key === 2) && data instanceof List) { 19 | let i = 0; 20 | for (const value of data) { 21 | if (i === key) return new Ok(new Some(value)); 22 | i++; 23 | } 24 | return new Error("Indexable"); 25 | } 26 | 27 | // Arrays and objects can be indexed 28 | if ( 29 | (int && Array.isArray(data)) || 30 | (data && typeof data === "object") || 31 | (data && Object.getPrototypeOf(data) === Object.prototype) 32 | ) { 33 | if (key in data) return new Ok(new Some(data[key])); 34 | return new Ok(new None()); 35 | } 36 | 37 | return new Error(int ? "Indexable" : "Dict"); 38 | } 39 | 40 | export function index(data, key) { 41 | const int = Number.isInteger(key); 42 | 43 | // Dictionaries and dictionary-like objects can be indexed 44 | if (data instanceof Dict || data instanceof WeakMap || data instanceof Map) { 45 | const entry = data.get(key, undefined); 46 | return new Ok(entry); 47 | } 48 | 49 | // The first 3 elements of lists can be indexed 50 | if ((key === 1 || key === 2) && data instanceof List) { 51 | let i = 0; 52 | for (const value of data) { 53 | if (i === key) return new Ok(value); 54 | i++; 55 | } 56 | return new Error("Indexable"); 57 | } 58 | 59 | // Arrays and objects can be indexed 60 | if ( 61 | (int && Array.isArray(data)) || 62 | (data && typeof data === "object") || 63 | (data && Object.getPrototypeOf(data) === Object.prototype) 64 | ) { 65 | return new Ok(data[key]); 66 | } 67 | 68 | return new Error(int ? "Indexable" : "Dict"); 69 | } 70 | 71 | export function list(data, decode, pushPath, index, emptyList) { 72 | if (!(data instanceof List || Array.isArray(data))) { 73 | let error = new DecodeError("List", classify(data), emptyList); 74 | return [emptyList, List.fromArray([error])]; 75 | } 76 | 77 | const decoded = []; 78 | 79 | for (const element of data) { 80 | const layer = decode(element); 81 | const [out, errors] = layer; 82 | 83 | if (errors instanceof NonEmpty) { 84 | const [_, errors] = pushPath(layer, index.toString()); 85 | return [emptyList, errors]; 86 | } 87 | decoded.push(out); 88 | index++; 89 | } 90 | 91 | return [List.fromArray(decoded), emptyList]; 92 | } 93 | 94 | export function dict(data) { 95 | if (data instanceof Dict) { 96 | return new Ok(data); 97 | } 98 | if (data instanceof Map || data instanceof WeakMap) { 99 | return new Ok(Dict.fromMap(data)); 100 | } 101 | if (data == null) { 102 | return new Error("Dict"); 103 | } 104 | if (typeof data !== "object") { 105 | return new Error("Dict"); 106 | } 107 | const proto = Object.getPrototypeOf(data); 108 | if (proto === Object.prototype || proto === null) { 109 | return new Ok(Dict.fromObject(data)); 110 | } 111 | return new Error("Dict"); 112 | } 113 | -------------------------------------------------------------------------------- /test/decode/zero_test.gleam: -------------------------------------------------------------------------------- 1 | import decode/zero 2 | import gleam/dict 3 | import gleam/dynamic.{type Dynamic, DecodeError} 4 | import gleam/float 5 | import gleam/int 6 | import gleam/option 7 | import gleeunit/should 8 | 9 | pub type User { 10 | User( 11 | name: String, 12 | email: String, 13 | is_admin: Bool, 14 | is_confirmed: Bool, 15 | score: Int, 16 | ) 17 | } 18 | 19 | pub fn decoder_test() { 20 | let data = 21 | dynamic.from( 22 | dict.from_list([ 23 | #("name", dynamic.from("Nubi")), 24 | #("email", dynamic.from("nubi@example.com")), 25 | #("is_admin", dynamic.from(False)), 26 | #("is_confirmed", dynamic.from(True)), 27 | #("score", dynamic.from(180)), 28 | ]), 29 | ) 30 | 31 | let decoder = { 32 | use name <- zero.field("name", zero.string) 33 | use email <- zero.field("email", zero.string) 34 | use is_admin <- zero.field("is_admin", zero.bool) 35 | use is_confirmed <- zero.field("is_confirmed", zero.bool) 36 | use score <- zero.field("score", zero.int) 37 | zero.success(User(name:, email:, is_admin:, is_confirmed:, score:)) 38 | } 39 | 40 | zero.run(data, decoder) 41 | |> should.be_ok 42 | |> should.equal(User("Nubi", "nubi@example.com", False, True, 180)) 43 | } 44 | 45 | pub fn field_ok_test() { 46 | let data = dynamic.from(dict.from_list([#("name", dynamic.from("Nubi"))])) 47 | let decoder = { 48 | use name <- zero.field("name", zero.string) 49 | zero.success(name) 50 | } 51 | 52 | zero.run(data, decoder) 53 | |> should.be_ok 54 | |> should.equal("Nubi") 55 | } 56 | 57 | pub fn subfield_ok_test() { 58 | let data = 59 | dynamic.from( 60 | dict.from_list([ 61 | #("person", dict.from_list([#("name", dynamic.from("Nubi"))])), 62 | ]), 63 | ) 64 | let decoder = { 65 | use name <- zero.subfield(["person", "name"], zero.string) 66 | zero.success(name) 67 | } 68 | 69 | zero.run(data, decoder) 70 | |> should.be_ok 71 | |> should.equal("Nubi") 72 | } 73 | 74 | pub fn field_int_index_ok_test() { 75 | let decoder = { 76 | use x <- zero.field(0, zero.string) 77 | use y <- zero.field(1, zero.string) 78 | zero.success(#(x, y)) 79 | } 80 | 81 | dynamic.from(#("one", "two", "three")) 82 | |> zero.run(decoder) 83 | |> should.be_ok 84 | |> should.equal(#("one", "two")) 85 | } 86 | 87 | pub fn field_int_index_list_ok_test() { 88 | let decoder = { 89 | use x <- zero.field(0, zero.string) 90 | use y <- zero.field(1, zero.string) 91 | zero.success(#(x, y)) 92 | } 93 | 94 | dynamic.from(["one", "two", "three", "four"]) 95 | |> zero.run(decoder) 96 | |> should.be_ok 97 | |> should.equal(#("one", "two")) 98 | } 99 | 100 | pub fn subfield_not_found_error_test() { 101 | let decoder = { 102 | use name <- zero.subfield(["name"], zero.string) 103 | zero.success(name) 104 | } 105 | 106 | dynamic.from(123) 107 | |> zero.run(decoder) 108 | |> should.be_error 109 | |> should.equal([DecodeError("Dict", "Int", [])]) 110 | } 111 | 112 | pub fn field_not_found_error_test() { 113 | let decoder = { 114 | use name <- zero.subfield(["name"], zero.string) 115 | zero.success(name) 116 | } 117 | 118 | dynamic.from(123) 119 | |> zero.run(decoder) 120 | |> should.be_error 121 | |> should.equal([DecodeError("Dict", "Int", [])]) 122 | } 123 | 124 | pub fn field_wrong_inner_error_test() { 125 | let decoder = { 126 | use name <- zero.field("name", zero.string) 127 | zero.success(name) 128 | } 129 | 130 | dynamic.from(dict.from_list([#("name", dynamic.from(123))])) 131 | |> zero.run(decoder) 132 | |> should.be_error 133 | |> should.equal([DecodeError("String", "Int", ["name"])]) 134 | } 135 | 136 | pub fn subfield_int_index_ok_test() { 137 | let decoder = { 138 | use x <- zero.subfield([0, 1], zero.string) 139 | use y <- zero.subfield([1, 0], zero.string) 140 | zero.success(#(x, y)) 141 | } 142 | 143 | dynamic.from(#(#("one", "two", "three"), #("a", "b"))) 144 | |> zero.run(decoder) 145 | |> should.be_ok 146 | |> should.equal(#("two", "a")) 147 | } 148 | 149 | pub fn subfield_wrong_inner_error_test() { 150 | let data = dynamic.from(dict.from_list([#("name", dynamic.from(123))])) 151 | zero.run(data, { 152 | use name <- zero.field("name", zero.string) 153 | zero.success(name) 154 | }) 155 | |> should.be_error 156 | |> should.equal([DecodeError("String", "Int", ["name"])]) 157 | } 158 | 159 | pub fn string_ok_test() { 160 | dynamic.from("Hello!") 161 | |> zero.run(zero.string) 162 | |> should.be_ok 163 | |> should.equal("Hello!") 164 | } 165 | 166 | pub fn string_error_test() { 167 | dynamic.from(123) 168 | |> zero.run(zero.string) 169 | |> should.be_error 170 | |> should.equal([DecodeError("String", "Int", [])]) 171 | } 172 | 173 | pub fn dynamic_test() { 174 | let data = dynamic.from(123) 175 | data 176 | |> zero.run(zero.dynamic) 177 | |> should.be_ok 178 | |> should.equal(data) 179 | } 180 | 181 | pub fn int_ok_test() { 182 | dynamic.from(123) 183 | |> zero.run(zero.int) 184 | |> should.be_ok 185 | |> should.equal(123) 186 | } 187 | 188 | pub fn int_error_test() { 189 | dynamic.from("123") 190 | |> zero.run(zero.int) 191 | |> should.be_error 192 | |> should.equal([DecodeError("Int", "String", [])]) 193 | } 194 | 195 | pub fn float_ok_test() { 196 | dynamic.from(123.45) 197 | |> zero.run(zero.float) 198 | |> should.be_ok 199 | |> should.equal(123.45) 200 | } 201 | 202 | pub fn float_error_test() { 203 | dynamic.from("123.45") 204 | |> zero.run(zero.float) 205 | |> should.be_error 206 | |> should.equal([DecodeError("Float", "String", [])]) 207 | } 208 | 209 | pub fn bool_true_test() { 210 | dynamic.from(True) 211 | |> zero.run(zero.bool) 212 | |> should.be_ok 213 | |> should.equal(True) 214 | } 215 | 216 | pub fn bool_false_test() { 217 | dynamic.from(False) 218 | |> zero.run(zero.bool) 219 | |> should.be_ok 220 | |> should.equal(False) 221 | } 222 | 223 | pub fn bool_error_test() { 224 | dynamic.from(123) 225 | |> zero.run(zero.bool) 226 | |> should.be_error 227 | |> should.equal([DecodeError("Bool", "Int", [])]) 228 | } 229 | 230 | pub fn bit_array_ok_test() { 231 | dynamic.from(<<1, 5, 3>>) 232 | |> zero.run(zero.bit_array) 233 | |> should.be_ok 234 | |> should.equal(<<1, 5, 3>>) 235 | } 236 | 237 | pub fn bit_array_error_test() { 238 | dynamic.from(123) 239 | |> zero.run(zero.bit_array) 240 | |> should.be_error 241 | |> should.equal([DecodeError("BitArray", "Int", [])]) 242 | } 243 | 244 | pub fn list_tuple_ok_test() { 245 | dynamic.from(#("Hello", "Joe")) 246 | |> zero.run(zero.list(zero.string)) 247 | |> should.be_ok 248 | |> should.equal(["Hello", "Joe"]) 249 | } 250 | 251 | pub fn list_string_ok_test() { 252 | dynamic.from(["Hello", "Joe"]) 253 | |> zero.run(zero.list(zero.string)) 254 | |> should.be_ok 255 | |> should.equal(["Hello", "Joe"]) 256 | } 257 | 258 | pub fn list_bool_ok_test() { 259 | dynamic.from([True, False]) 260 | |> zero.run(zero.list(zero.bool)) 261 | |> should.be_ok 262 | |> should.equal([True, False]) 263 | } 264 | 265 | pub fn list_error_test() { 266 | dynamic.from(123) 267 | |> zero.run(zero.list(zero.int)) 268 | |> should.be_error 269 | |> should.equal([DecodeError("List", "Int", [])]) 270 | } 271 | 272 | pub fn list_inner_0_error_test() { 273 | dynamic.from([1, 2]) 274 | |> zero.run(zero.list(zero.string)) 275 | |> should.be_error 276 | |> should.equal([DecodeError("String", "Int", ["0"])]) 277 | } 278 | 279 | pub fn list_inner_1_error_test() { 280 | dynamic.from([dynamic.from("1"), dynamic.from(2)]) 281 | |> zero.run(zero.list(zero.string)) 282 | |> should.be_error 283 | |> should.equal([DecodeError("String", "Int", ["1"])]) 284 | } 285 | 286 | pub fn list_tuple_inner_1_error_test() { 287 | dynamic.from(#("1", 2)) 288 | |> zero.run(zero.list(zero.string)) 289 | |> should.be_error 290 | |> should.equal([DecodeError("String", "Int", ["1"])]) 291 | } 292 | 293 | pub fn dict_ok_test() { 294 | let values = dict.from_list([#("first", 1), #("second", 2)]) 295 | dynamic.from(values) 296 | |> zero.run(zero.dict(zero.string, zero.int)) 297 | |> should.be_ok 298 | |> should.equal(values) 299 | } 300 | 301 | pub fn dict_value_error_test() { 302 | dynamic.from(dict.from_list([#(1.1, 1), #(1.2, 2)])) 303 | |> zero.run(zero.dict(zero.float, zero.string)) 304 | |> should.be_error 305 | |> should.equal([DecodeError("String", "Int", ["values"])]) 306 | } 307 | 308 | pub fn dict_key_error_test() { 309 | dynamic.from(dict.from_list([#(1.1, 1), #(1.2, 2)])) 310 | |> zero.run(zero.dict(zero.string, zero.int)) 311 | |> should.be_error 312 | |> should.equal([DecodeError("String", "Float", ["keys"])]) 313 | } 314 | 315 | pub fn dict_error_test() { 316 | dynamic.from(123) 317 | |> zero.run(zero.dict(zero.string, zero.int)) 318 | |> should.be_error 319 | |> should.equal([DecodeError("Dict", "Int", [])]) 320 | } 321 | 322 | pub fn at_dict_string_ok_test() { 323 | dynamic.from( 324 | dict.from_list([ 325 | #( 326 | "first", 327 | dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), 328 | ), 329 | ]), 330 | ) 331 | |> zero.run(zero.at(["first", "second", "third"], zero.int)) 332 | |> should.be_ok 333 | |> should.equal(1337) 334 | } 335 | 336 | pub fn at_dict_int_ok_test() { 337 | dynamic.from( 338 | dict.from_list([ 339 | #(10, dict.from_list([#(20, dict.from_list([#(30, 1337)]))])), 340 | ]), 341 | ) 342 | |> zero.run(zero.at([10, 20, 30], zero.int)) 343 | |> should.be_ok 344 | |> should.equal(1337) 345 | } 346 | 347 | pub fn at_tuple_int_ok_test() { 348 | dynamic.from(#("x", #("a", "b", "c"), "z")) 349 | |> zero.run(zero.at([1, 0], zero.string)) 350 | |> should.be_ok 351 | |> should.equal("a") 352 | } 353 | 354 | pub fn at_wrong_inner_error_test() { 355 | dynamic.from( 356 | dict.from_list([ 357 | #( 358 | "first", 359 | dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), 360 | ), 361 | ]), 362 | ) 363 | |> zero.run(zero.at(["first", "second", "third"], zero.string)) 364 | |> should.be_error 365 | |> should.equal([DecodeError("String", "Int", ["first", "second", "third"])]) 366 | } 367 | 368 | pub fn at_no_path_error_test() { 369 | dynamic.from(dict.from_list([#("first", dict.from_list([#("third", 1337)]))])) 370 | |> zero.run(zero.at(["first", "second", "third"], zero.int)) 371 | |> should.be_error 372 | |> should.equal([DecodeError("Field", "Nothing", ["first", "second"])]) 373 | } 374 | 375 | pub fn optional_string_present_ok_test() { 376 | dynamic.from("Hello, Joe!") 377 | |> zero.run(zero.optional(zero.string)) 378 | |> should.be_ok 379 | |> should.equal(option.Some("Hello, Joe!")) 380 | } 381 | 382 | pub fn optional_bool_present_ok_test() { 383 | dynamic.from(True) 384 | |> zero.run(zero.optional(zero.bool)) 385 | |> should.be_ok 386 | |> should.equal(option.Some(True)) 387 | } 388 | 389 | pub fn optional_bool_absent_nil_ok_test() { 390 | dynamic.from(Nil) 391 | |> zero.run(zero.optional(zero.bool)) 392 | |> should.be_ok 393 | |> should.equal(option.None) 394 | } 395 | 396 | pub fn optional_bool_absent_none_ok_test() { 397 | dynamic.from(option.None) 398 | |> zero.run(zero.optional(zero.bool)) 399 | |> should.be_ok 400 | |> should.equal(option.None) 401 | } 402 | 403 | pub fn optional_error_test() { 404 | dynamic.from(123) 405 | |> zero.run(zero.optional(zero.string)) 406 | |> should.be_error 407 | |> should.equal([DecodeError("String", "Int", [])]) 408 | } 409 | 410 | pub fn map_test() { 411 | dynamic.from(123) 412 | |> zero.run(zero.int |> zero.map(int.to_string)) 413 | |> should.be_ok 414 | |> should.equal("123") 415 | } 416 | 417 | pub fn map_errors_test() { 418 | let decoder = 419 | zero.at( 420 | ["data"], 421 | zero.map_errors(zero.string, fn(errors) { 422 | let assert [DecodeError("String", "Int", [])] = errors 423 | [ 424 | DecodeError("Wibble", "Wobble", ["ok"]), 425 | DecodeError("Wabble", "Wubble", ["ok"]), 426 | ] 427 | }), 428 | ) 429 | 430 | dynamic.from(dict.from_list([#("data", 123)])) 431 | |> zero.run(decoder) 432 | |> should.be_error 433 | |> should.equal([ 434 | DecodeError("Wibble", "Wobble", ["data", "ok"]), 435 | DecodeError("Wabble", "Wubble", ["data", "ok"]), 436 | ]) 437 | } 438 | 439 | pub fn collapse_errors_test() { 440 | dynamic.from(dict.from_list([#("data", 123)])) 441 | |> zero.run(zero.at(["data"], zero.string |> zero.collapse_errors("Wibble"))) 442 | |> should.be_error 443 | |> should.equal([DecodeError("Wibble", "Int", ["data"])]) 444 | } 445 | 446 | pub fn then_test() { 447 | let decoder = 448 | zero.at(["key"], zero.int) 449 | |> zero.then(fn(i) { 450 | zero.at(["value"], case i { 451 | 1 -> zero.int |> zero.map(AnInt) 452 | _ -> zero.string |> zero.map(AString) 453 | }) 454 | }) 455 | 456 | dynamic.from(dict.from_list([#("key", 1), #("value", 100)])) 457 | |> zero.run(decoder) 458 | |> should.be_ok 459 | |> should.equal(AnInt(100)) 460 | 461 | dynamic.from( 462 | dict.from_list([#("key", dynamic.from(2)), #("value", dynamic.from("Hi!"))]), 463 | ) 464 | |> zero.run(decoder) 465 | |> should.be_ok 466 | |> should.equal(AString("Hi!")) 467 | } 468 | 469 | type IntOrString { 470 | AnInt(Int) 471 | AString(String) 472 | } 473 | 474 | pub fn then_error_0_test() { 475 | let decoder = 476 | zero.at(["key"], zero.int) 477 | |> zero.then(fn(i) { 478 | zero.at(["value"], case i { 479 | 1 -> zero.int |> zero.map(AnInt) 480 | _ -> zero.string |> zero.map(AString) 481 | }) 482 | }) 483 | 484 | dynamic.from(123) 485 | |> zero.run(decoder) 486 | |> should.be_error 487 | |> should.equal([DecodeError("Dict", "Int", [])]) 488 | } 489 | 490 | pub fn then_error_1_test() { 491 | let decoder = 492 | zero.at(["key"], zero.int) 493 | |> zero.then(fn(i) { 494 | zero.at(["value"], case i { 495 | 1 -> zero.int |> zero.map(AnInt) 496 | _ -> zero.string |> zero.map(AString) 497 | }) 498 | }) 499 | 500 | dynamic.from( 501 | dict.from_list([#("key", dynamic.from(1)), #("value", dynamic.from("Hi!"))]), 502 | ) 503 | |> zero.run(decoder) 504 | |> should.be_error 505 | |> should.equal([DecodeError("Int", "String", ["value"])]) 506 | } 507 | 508 | pub type MyEnum { 509 | A 510 | B 511 | C 512 | } 513 | 514 | pub fn then_enum_test() { 515 | let decoder = 516 | zero.string 517 | |> zero.then(fn(s) { 518 | case s { 519 | "a" -> zero.success(A) 520 | "b" -> zero.success(B) 521 | "c" -> zero.success(C) 522 | _ -> zero.failure(A, "MyEnum") 523 | } 524 | }) 525 | 526 | zero.run(dynamic.from("a"), decoder) 527 | |> should.be_ok 528 | |> should.equal(A) 529 | 530 | zero.run(dynamic.from("b"), decoder) 531 | |> should.be_ok 532 | |> should.equal(B) 533 | 534 | zero.run(dynamic.from("c"), decoder) 535 | |> should.be_ok 536 | |> should.equal(C) 537 | 538 | zero.run(dynamic.from("d"), decoder) 539 | |> should.be_error 540 | |> should.equal([DecodeError("MyEnum", "String", [])]) 541 | } 542 | 543 | pub fn one_of_ok_0_test() { 544 | dynamic.from("Hello!") 545 | |> zero.run(zero.one_of(zero.string, [zero.int |> zero.map(int.to_string)])) 546 | |> should.be_ok 547 | |> should.equal("Hello!") 548 | } 549 | 550 | pub fn one_of_ok_1_test() { 551 | let decoder = 552 | zero.one_of(zero.string, [ 553 | zero.int 554 | |> zero.map(int.to_string), 555 | ]) 556 | dynamic.from(123) 557 | |> zero.run(decoder) 558 | |> should.be_ok 559 | |> should.equal("123") 560 | } 561 | 562 | pub fn one_of_ok_2_test() { 563 | let decoder = 564 | zero.one_of(zero.string, [ 565 | zero.int |> zero.map(int.to_string), 566 | zero.float |> zero.map(float.to_string), 567 | ]) 568 | dynamic.from(12.45) 569 | |> zero.run(decoder) 570 | |> should.be_ok 571 | |> should.equal("12.45") 572 | } 573 | 574 | pub fn one_of_error_test() { 575 | let decoder = 576 | zero.one_of(zero.string, or: [ 577 | zero.int 578 | |> zero.map(int.to_string), 579 | ]) 580 | dynamic.from(1.2) 581 | |> zero.run(decoder) 582 | |> should.be_error 583 | |> should.equal([DecodeError("String", "Float", [])]) 584 | } 585 | 586 | pub fn failure_test() { 587 | dynamic.from(123) 588 | |> zero.run(zero.failure(1, "WibbleWobble")) 589 | |> should.be_error 590 | |> should.equal([DecodeError("WibbleWobble", "Int", [])]) 591 | } 592 | 593 | pub fn variants_test() { 594 | let decoder = { 595 | use tag <- zero.field("tag", zero.string) 596 | case tag { 597 | "int" -> { 598 | use int <- zero.field("the-int", zero.int) 599 | zero.success(AnInt(int)) 600 | } 601 | "string" -> { 602 | use string <- zero.field("the-string", zero.string) 603 | zero.success(AString(string)) 604 | } 605 | _ -> { 606 | zero.failure(AnInt(0), "IntOrString") 607 | } 608 | } 609 | } 610 | 611 | // Int variant 612 | dynamic.from( 613 | dict.from_list([ 614 | #("tag", dynamic.from("int")), 615 | #("the-int", dynamic.from(123)), 616 | ]), 617 | ) 618 | |> zero.run(decoder) 619 | |> should.be_ok 620 | |> should.equal(AnInt(123)) 621 | 622 | // String variant 623 | dynamic.from( 624 | dict.from_list([ 625 | #("tag", dynamic.from("string")), 626 | #("the-string", dynamic.from("hello")), 627 | ]), 628 | ) 629 | |> zero.run(decoder) 630 | |> should.be_ok 631 | |> should.equal(AString("hello")) 632 | 633 | // Invalid tag 634 | dynamic.from( 635 | dict.from_list([ 636 | #("tag", dynamic.from("dunno")), 637 | #("the-string", dynamic.from("hello")), 638 | ]), 639 | ) 640 | |> zero.run(decoder) 641 | |> should.be_error 642 | |> should.equal([DecodeError("IntOrString", "Dict", [])]) 643 | 644 | // Missing tag 645 | dynamic.from(dict.from_list([#("the-string", dynamic.from("hello"))])) 646 | |> zero.run(decoder) 647 | |> should.be_error 648 | |> should.equal([ 649 | DecodeError("Field", "Nothing", ["tag"]), 650 | DecodeError("IntOrString", "Dict", []), 651 | ]) 652 | 653 | // String invalid field 654 | dynamic.from( 655 | dict.from_list([ 656 | #("tag", dynamic.from("string")), 657 | #("the-string", dynamic.from(12.3)), 658 | ]), 659 | ) 660 | |> zero.run(decoder) 661 | |> should.be_error 662 | |> should.equal([DecodeError("String", "Float", ["the-string"])]) 663 | } 664 | 665 | pub type PocketMonsterType { 666 | Fire 667 | Water 668 | Grass 669 | Electric 670 | } 671 | 672 | pub fn documentation_enum_example_test() { 673 | let decoder = { 674 | use decoded_string <- zero.then(zero.string) 675 | case decoded_string { 676 | // Return succeeding decoders for valid strings 677 | "fire" -> zero.success(Fire) 678 | "water" -> zero.success(Water) 679 | "grass" -> zero.success(Grass) 680 | "electric" -> zero.success(Electric) 681 | // Return a failing decoder for any other strings 682 | _ -> zero.failure(Fire, "PocketMonsterType") 683 | } 684 | } 685 | 686 | zero.run(dynamic.from("water"), decoder) 687 | |> should.be_ok 688 | |> should.equal(Water) 689 | 690 | zero.run(dynamic.from("wobble"), decoder) 691 | |> should.be_error 692 | |> should.equal([DecodeError("PocketMonsterType", "String", [])]) 693 | } 694 | 695 | pub type PocketMonsterPerson { 696 | Trainer(name: String, badge_count: Int) 697 | GymLeader(name: String, speciality: String) 698 | } 699 | 700 | pub fn documentation_variants_example_test() { 701 | let trainer_decoder = { 702 | use name <- zero.field("name", zero.string) 703 | use badge_count <- zero.field("badge-count", zero.int) 704 | zero.success(Trainer(name, badge_count)) 705 | } 706 | 707 | let gym_leader_decoder = { 708 | use name <- zero.field("name", zero.string) 709 | use speciality <- zero.field("speciality", zero.string) 710 | zero.success(GymLeader(name, speciality)) 711 | } 712 | 713 | let decoder = { 714 | use tag <- zero.field("type", zero.string) 715 | case tag { 716 | "gym-leader" -> gym_leader_decoder 717 | _ -> trainer_decoder 718 | } 719 | } 720 | 721 | // Trainer 722 | dynamic.from( 723 | dict.from_list([ 724 | #("type", dynamic.from("trainer")), 725 | #("name", dynamic.from("Ash")), 726 | #("badge-count", dynamic.from(8)), 727 | ]), 728 | ) 729 | |> zero.run(decoder) 730 | |> should.be_ok 731 | |> should.equal(Trainer("Ash", 8)) 732 | 733 | // Gym leader 734 | dynamic.from( 735 | dict.from_list([ 736 | #("type", dynamic.from("gym-leader")), 737 | #("name", dynamic.from("Brock")), 738 | #("speciality", dynamic.from("Rock")), 739 | ]), 740 | ) 741 | |> zero.run(decoder) 742 | |> should.be_ok 743 | |> should.equal(GymLeader("Brock", "Rock")) 744 | 745 | // Error 746 | dynamic.from( 747 | dict.from_list([ 748 | #("type", dynamic.from("gym-leader")), 749 | #("name", dynamic.from("Brock")), 750 | ]), 751 | ) 752 | |> zero.run(decoder) 753 | |> should.be_error 754 | |> should.equal([ 755 | DecodeError(expected: "Field", found: "Nothing", path: ["speciality"]), 756 | ]) 757 | } 758 | 759 | pub fn new_primitive_decoder_string_ok_test() { 760 | dynamic.from("Hello!") 761 | |> zero.run(zero.new_primitive_decoder(dynamic.string, "")) 762 | |> should.be_ok 763 | |> should.equal("Hello!") 764 | } 765 | 766 | pub fn new_primitive_decoder_string_error_test() { 767 | dynamic.from(123) 768 | |> zero.run(zero.new_primitive_decoder(dynamic.string, "")) 769 | |> should.be_error 770 | |> should.equal([DecodeError("String", "Int", [])]) 771 | } 772 | 773 | pub fn new_primitive_decoder_float_ok_test() { 774 | dynamic.from(12.4) 775 | |> zero.run(zero.new_primitive_decoder(dynamic.float, 0.0)) 776 | |> should.be_ok 777 | |> should.equal(12.4) 778 | } 779 | 780 | pub fn new_primitive_decoder_float_error_test() { 781 | dynamic.from("blah") 782 | |> zero.run(zero.new_primitive_decoder(dynamic.float, 0.0)) 783 | |> should.be_error 784 | |> should.equal([DecodeError("Float", "String", [])]) 785 | } 786 | 787 | pub type LinkedList { 788 | ListEmpty 789 | ListNonEmpty(element: Int, tail: LinkedList) 790 | } 791 | 792 | pub fn list_decoder() -> zero.Decoder(LinkedList) { 793 | use tag <- zero.field("type", zero.string) 794 | case tag { 795 | "list-non-empty" -> { 796 | use element <- zero.field("element", zero.int) 797 | use tail <- zero.field("tail", list_decoder()) 798 | zero.success(ListNonEmpty(element:, tail:)) 799 | } 800 | _ -> zero.success(ListEmpty) 801 | } 802 | } 803 | 804 | pub fn recursive_data_structure_test() { 805 | dynamic.from( 806 | dict.from_list([ 807 | #("type", dynamic.from("list-non-empty")), 808 | #("element", dynamic.from(1)), 809 | #( 810 | "tail", 811 | dynamic.from( 812 | dict.from_list([ 813 | #("type", dynamic.from("list-non-empty")), 814 | #("element", dynamic.from(2)), 815 | #( 816 | "tail", 817 | dynamic.from( 818 | dict.from_list([#("type", dynamic.from("list-empty"))]), 819 | ), 820 | ), 821 | ]), 822 | ), 823 | ), 824 | ]), 825 | ) 826 | |> zero.run(list_decoder()) 827 | |> should.be_ok 828 | |> should.equal(ListNonEmpty(1, ListNonEmpty(2, ListEmpty))) 829 | } 830 | 831 | pub fn optionally_at_dict_string_ok_test() { 832 | dynamic.from( 833 | dict.from_list([ 834 | #( 835 | "first", 836 | dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), 837 | ), 838 | ]), 839 | ) 840 | |> zero.run(zero.optionally_at(["first", "second", "third"], 100, zero.int)) 841 | |> should.be_ok 842 | |> should.equal(1337) 843 | } 844 | 845 | pub fn optionally_at_dict_int_ok_test() { 846 | dynamic.from( 847 | dict.from_list([ 848 | #(10, dict.from_list([#(20, dict.from_list([#(30, 1337)]))])), 849 | ]), 850 | ) 851 | |> zero.run(zero.optionally_at([10, 20, 30], 123, zero.int)) 852 | |> should.be_ok 853 | |> should.equal(1337) 854 | } 855 | 856 | pub fn optionally_at_tuple_int_ok_test() { 857 | dynamic.from(#("x", #("a", "b", "c"), "z")) 858 | |> zero.run(zero.optionally_at([1, 0], "something", zero.string)) 859 | |> should.be_ok 860 | |> should.equal("a") 861 | } 862 | 863 | pub fn optionally_at_wrong_inner_error_test() { 864 | dynamic.from( 865 | dict.from_list([ 866 | #( 867 | "first", 868 | dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), 869 | ), 870 | ]), 871 | ) 872 | |> zero.run(zero.optionally_at( 873 | ["first", "second", "third"], 874 | "default", 875 | zero.string, 876 | )) 877 | |> should.be_error 878 | |> should.equal([DecodeError("String", "Int", ["first", "second", "third"])]) 879 | } 880 | 881 | pub fn optionally_at_no_path_error_test() { 882 | dynamic.from(dict.from_list([#("first", dict.from_list([#("third", 1337)]))])) 883 | |> zero.run(zero.optionally_at(["first", "second", "third"], 100, zero.int)) 884 | |> should.be_ok 885 | |> should.equal(100) 886 | } 887 | 888 | @external(erlang, "maps", "from_list") 889 | @external(javascript, "../decode_zero_test_ffi.mjs", "object") 890 | fn make_object(items: List(#(String, t))) -> Dynamic 891 | 892 | @external(erlang, "maps", "from_list") 893 | @external(javascript, "../decode_zero_test_ffi.mjs", "map") 894 | fn make_map(items: List(#(String, t))) -> Dynamic 895 | 896 | pub fn js_object_test() { 897 | [#("a", 10), #("b", 20), #("c", 30)] 898 | |> make_object 899 | |> zero.run(zero.dict(zero.string, zero.int)) 900 | |> should.be_ok 901 | |> should.equal(dict.from_list([#("a", 10), #("b", 20), #("c", 30)])) 902 | } 903 | 904 | pub fn js_map_test() { 905 | [#("a", 10), #("b", 20), #("c", 30)] 906 | |> make_map 907 | |> zero.run(zero.dict(zero.string, zero.int)) 908 | |> should.be_ok 909 | |> should.equal(dict.from_list([#("a", 10), #("b", 20), #("c", 30)])) 910 | } 911 | 912 | type Nested { 913 | Nested(List(Nested)) 914 | Value(String) 915 | } 916 | 917 | fn recursive_decoder() -> zero.Decoder(Nested) { 918 | use <- zero.recursive() 919 | zero.one_of(zero.string |> zero.map(Value), [ 920 | zero.list(recursive_decoder()) |> zero.map(Nested), 921 | ]) 922 | } 923 | 924 | pub fn recursive_test() { 925 | let nested = [["one", "two"], ["three"], []] 926 | let expected = 927 | Nested([ 928 | Nested([Value("one"), Value("two")]), 929 | Nested([Value("three")]), 930 | Nested([]), 931 | ]) 932 | 933 | zero.run(dynamic.from(nested), recursive_decoder()) 934 | |> should.be_ok 935 | |> should.equal(expected) 936 | } 937 | -------------------------------------------------------------------------------- /test/decode_test.gleam: -------------------------------------------------------------------------------- 1 | import decode 2 | import gleam/dict 3 | import gleam/dynamic.{DecodeError} 4 | import gleam/int 5 | import gleam/option 6 | import gleeunit 7 | import gleeunit/should 8 | 9 | pub fn main() { 10 | gleeunit.main() 11 | } 12 | 13 | pub type User { 14 | User( 15 | name: String, 16 | email: String, 17 | is_admin: Bool, 18 | is_confirmed: Bool, 19 | score: Int, 20 | ) 21 | } 22 | 23 | pub fn decoder_test() { 24 | let data = 25 | dynamic.from( 26 | dict.from_list([ 27 | #("name", dynamic.from("Nubi")), 28 | #("email", dynamic.from("nubi@example.com")), 29 | #("is_admin", dynamic.from(False)), 30 | #("is_confirmed", dynamic.from(True)), 31 | #("score", dynamic.from(180)), 32 | ]), 33 | ) 34 | 35 | let result = 36 | decode.into({ 37 | use name <- decode.parameter 38 | use email <- decode.parameter 39 | use is_admin <- decode.parameter 40 | use is_confirmed <- decode.parameter 41 | use score <- decode.parameter 42 | User(name, email, is_admin, is_confirmed, score) 43 | }) 44 | |> decode.field("name", decode.string) 45 | |> decode.field("email", decode.string) 46 | |> decode.field("is_admin", decode.bool) 47 | |> decode.field("is_confirmed", decode.bool) 48 | |> decode.field("score", decode.int) 49 | |> decode.from(data) 50 | 51 | result 52 | |> should.be_ok 53 | |> should.equal(User("Nubi", "nubi@example.com", False, True, 180)) 54 | } 55 | 56 | pub fn field_ok_test() { 57 | let data = dynamic.from(dict.from_list([#("name", dynamic.from("Nubi"))])) 58 | let result = 59 | decode.into(fn(x) { x }) 60 | |> decode.field("name", decode.string) 61 | |> decode.from(data) 62 | result 63 | |> should.be_ok 64 | |> should.equal("Nubi") 65 | } 66 | 67 | pub fn field_int_index_ok_test() { 68 | let data = dynamic.from(#("one", "two", "three")) 69 | let result = 70 | decode.into({ 71 | use x <- decode.parameter 72 | use y <- decode.parameter 73 | #(x, y) 74 | }) 75 | |> decode.field(0, decode.string) 76 | |> decode.field(1, decode.string) 77 | |> decode.from(data) 78 | result 79 | |> should.be_ok 80 | |> should.equal(#("one", "two")) 81 | } 82 | 83 | pub fn field_not_found_error_test() { 84 | let data = dynamic.from(123) 85 | let result = 86 | decode.into(fn(x) { x }) 87 | |> decode.field("name", decode.string) 88 | |> decode.from(data) 89 | result 90 | |> should.be_error 91 | |> should.equal([DecodeError("Dict", "Int", [])]) 92 | } 93 | 94 | pub fn field_wrong_inner_error_test() { 95 | let data = dynamic.from(dict.from_list([#("name", dynamic.from(123))])) 96 | let result = 97 | decode.into(fn(x) { x }) 98 | |> decode.field("name", decode.string) 99 | |> decode.from(data) 100 | result 101 | |> should.be_error 102 | |> should.equal([DecodeError("String", "Int", ["name"])]) 103 | } 104 | 105 | pub fn subfield_ok_test() { 106 | let data = 107 | dynamic.from( 108 | dict.from_list([ 109 | #("data", dict.from_list([#("name", dynamic.from("Nubi"))])), 110 | ]), 111 | ) 112 | let result = 113 | decode.into(fn(x) { x }) 114 | |> decode.subfield(["data", "name"], decode.string) 115 | |> decode.from(data) 116 | result 117 | |> should.be_ok 118 | |> should.equal("Nubi") 119 | } 120 | 121 | pub fn subfield_int_index_ok_test() { 122 | let data = dynamic.from(#(#("one", "two", "three"), #("a", "b"))) 123 | let result = 124 | decode.into({ 125 | use x <- decode.parameter 126 | use y <- decode.parameter 127 | #(x, y) 128 | }) 129 | |> decode.subfield([0, 1], decode.string) 130 | |> decode.subfield([1, 0], decode.string) 131 | |> decode.from(data) 132 | result 133 | |> should.be_ok 134 | |> should.equal(#("two", "a")) 135 | } 136 | 137 | pub fn subfield_not_found_error_test() { 138 | let data = dynamic.from(123) 139 | let result = 140 | decode.into(fn(x) { x }) 141 | |> decode.subfield(["name"], decode.string) 142 | |> decode.from(data) 143 | result 144 | |> should.be_error 145 | |> should.equal([DecodeError("Dict", "Int", [])]) 146 | } 147 | 148 | pub fn subfield_wrong_inner_error_test() { 149 | let data = dynamic.from(dict.from_list([#("name", dynamic.from(123))])) 150 | let result = 151 | decode.into(fn(x) { x }) 152 | |> decode.subfield(["name"], decode.string) 153 | |> decode.from(data) 154 | result 155 | |> should.be_error 156 | |> should.equal([DecodeError("String", "Int", ["name"])]) 157 | } 158 | 159 | pub fn string_ok_test() { 160 | let data = dynamic.from("Hello!") 161 | decode.string 162 | |> decode.from(data) 163 | |> should.be_ok 164 | |> should.equal("Hello!") 165 | } 166 | 167 | pub fn string_error_test() { 168 | let data = dynamic.from(123) 169 | decode.string 170 | |> decode.from(data) 171 | |> should.be_error 172 | |> should.equal([DecodeError("String", "Int", [])]) 173 | } 174 | 175 | pub fn dynamic_test() { 176 | let data = dynamic.from(123) 177 | decode.dynamic 178 | |> decode.from(data) 179 | |> should.be_ok 180 | |> should.equal(data) 181 | } 182 | 183 | pub fn int_ok_test() { 184 | let data = dynamic.from(123) 185 | decode.int 186 | |> decode.from(data) 187 | |> should.be_ok 188 | |> should.equal(123) 189 | } 190 | 191 | pub fn int_error_test() { 192 | let data = dynamic.from("123") 193 | decode.int 194 | |> decode.from(data) 195 | |> should.be_error 196 | |> should.equal([DecodeError("Int", "String", [])]) 197 | } 198 | 199 | pub fn float_ok_test() { 200 | let data = dynamic.from(123.45) 201 | decode.float 202 | |> decode.from(data) 203 | |> should.be_ok 204 | |> should.equal(123.45) 205 | } 206 | 207 | pub fn float_error_test() { 208 | let data = dynamic.from("123.45") 209 | decode.float 210 | |> decode.from(data) 211 | |> should.be_error 212 | |> should.equal([DecodeError("Float", "String", [])]) 213 | } 214 | 215 | pub fn bool_true_test() { 216 | let data = dynamic.from(True) 217 | decode.bool 218 | |> decode.from(data) 219 | |> should.be_ok 220 | |> should.equal(True) 221 | } 222 | 223 | pub fn bool_false_test() { 224 | let data = dynamic.from(False) 225 | decode.bool 226 | |> decode.from(data) 227 | |> should.be_ok 228 | |> should.equal(False) 229 | } 230 | 231 | pub fn bool_error_test() { 232 | let data = dynamic.from(123) 233 | decode.bool 234 | |> decode.from(data) 235 | |> should.be_error 236 | |> should.equal([DecodeError("Bool", "Int", [])]) 237 | } 238 | 239 | pub fn bit_array_ok_test() { 240 | let data = dynamic.from(<<1, 5, 3>>) 241 | decode.bit_array 242 | |> decode.from(data) 243 | |> should.be_ok 244 | |> should.equal(<<1, 5, 3>>) 245 | } 246 | 247 | pub fn bit_array_error_test() { 248 | let data = dynamic.from(123) 249 | decode.bit_array 250 | |> decode.from(data) 251 | |> should.be_error 252 | |> should.equal([DecodeError("BitArray", "Int", [])]) 253 | } 254 | 255 | pub fn list_string_ok_test() { 256 | let data = dynamic.from(["Hello", "Joe"]) 257 | decode.list(decode.string) 258 | |> decode.from(data) 259 | |> should.be_ok 260 | |> should.equal(["Hello", "Joe"]) 261 | } 262 | 263 | pub fn list_bool_ok_test() { 264 | let data = dynamic.from([True, False]) 265 | decode.list(decode.bool) 266 | |> decode.from(data) 267 | |> should.be_ok 268 | |> should.equal([True, False]) 269 | } 270 | 271 | pub fn list_error_test() { 272 | let data = dynamic.from(123) 273 | decode.list(decode.int) 274 | |> decode.from(data) 275 | |> should.be_error 276 | |> should.equal([DecodeError("List", "Int", [])]) 277 | } 278 | 279 | pub fn list_inner_0_error_test() { 280 | let data = dynamic.from([1, 2]) 281 | decode.list(decode.string) 282 | |> decode.from(data) 283 | |> should.be_error 284 | |> should.equal([DecodeError("String", "Int", ["*"])]) 285 | } 286 | 287 | pub fn list_inner_1_error_test() { 288 | let data = dynamic.from([dynamic.from("1"), dynamic.from(2)]) 289 | decode.list(decode.string) 290 | |> decode.from(data) 291 | |> should.be_error 292 | |> should.equal([DecodeError("String", "Int", ["*"])]) 293 | } 294 | 295 | pub fn dict_ok_test() { 296 | let values = dict.from_list([#("first", 1), #("second", 2)]) 297 | let data = dynamic.from(values) 298 | decode.dict(decode.string, decode.int) 299 | |> decode.from(data) 300 | |> should.be_ok 301 | |> should.equal(values) 302 | } 303 | 304 | pub fn dict_value_error_test() { 305 | let data = dynamic.from(dict.from_list([#(1.1, 1), #(1.2, 2)])) 306 | decode.dict(decode.float, decode.string) 307 | |> decode.from(data) 308 | |> should.be_error 309 | |> should.equal([DecodeError("String", "Int", ["values"])]) 310 | } 311 | 312 | pub fn dict_key_error_test() { 313 | let data = dynamic.from(dict.from_list([#(1.1, 1), #(1.2, 2)])) 314 | decode.dict(decode.string, decode.int) 315 | |> decode.from(data) 316 | |> should.be_error 317 | |> should.equal([DecodeError("String", "Float", ["keys"])]) 318 | } 319 | 320 | pub fn dict_error_test() { 321 | let data = dynamic.from(123) 322 | decode.dict(decode.string, decode.int) 323 | |> decode.from(data) 324 | |> should.be_error 325 | |> should.equal([DecodeError("Dict", "Int", [])]) 326 | } 327 | 328 | pub fn at_dict_string_ok_test() { 329 | let data = 330 | dynamic.from( 331 | dict.from_list([ 332 | #( 333 | "first", 334 | dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), 335 | ), 336 | ]), 337 | ) 338 | decode.at(["first", "second", "third"], decode.int) 339 | |> decode.from(data) 340 | |> should.be_ok 341 | |> should.equal(1337) 342 | } 343 | 344 | pub fn at_dict_int_ok_test() { 345 | let data = 346 | dynamic.from( 347 | dict.from_list([ 348 | #(10, dict.from_list([#(20, dict.from_list([#(30, 1337)]))])), 349 | ]), 350 | ) 351 | decode.at([10, 20, 30], decode.int) 352 | |> decode.from(data) 353 | |> should.be_ok 354 | |> should.equal(1337) 355 | } 356 | 357 | pub fn at_tuple_int_ok_test() { 358 | let data = dynamic.from(#("x", #("a", "b", "c"), "z")) 359 | decode.at([1, 0], decode.string) 360 | |> decode.from(data) 361 | |> should.be_ok 362 | |> should.equal("a") 363 | } 364 | 365 | pub fn at_wrong_inner_error_test() { 366 | let data = 367 | dynamic.from( 368 | dict.from_list([ 369 | #( 370 | "first", 371 | dict.from_list([#("second", dict.from_list([#("third", 1337)]))]), 372 | ), 373 | ]), 374 | ) 375 | decode.at(["first", "second", "third"], decode.string) 376 | |> decode.from(data) 377 | |> should.be_error 378 | |> should.equal([DecodeError("String", "Int", ["first", "second", "third"])]) 379 | } 380 | 381 | pub fn at_no_path_error_test() { 382 | let data = 383 | dynamic.from( 384 | dict.from_list([#("first", dict.from_list([#("third", 1337)]))]), 385 | ) 386 | decode.at(["first", "second", "third"], decode.int) 387 | |> decode.from(data) 388 | |> should.be_error 389 | |> should.equal([DecodeError("Dict", "Nil", ["first", "second"])]) 390 | } 391 | 392 | pub fn optional_string_present_ok_test() { 393 | let data = dynamic.from("Hello, Joe!") 394 | decode.optional(decode.string) 395 | |> decode.from(data) 396 | |> should.be_ok 397 | |> should.equal(option.Some("Hello, Joe!")) 398 | } 399 | 400 | pub fn optional_bool_present_ok_test() { 401 | let data = dynamic.from(True) 402 | decode.optional(decode.bool) 403 | |> decode.from(data) 404 | |> should.be_ok 405 | |> should.equal(option.Some(True)) 406 | } 407 | 408 | pub fn optional_bool_absent_nil_ok_test() { 409 | let data = dynamic.from(Nil) 410 | decode.optional(decode.bool) 411 | |> decode.from(data) 412 | |> should.be_ok 413 | |> should.equal(option.None) 414 | } 415 | 416 | pub fn optional_bool_absent_none_ok_test() { 417 | let data = dynamic.from(option.None) 418 | decode.optional(decode.bool) 419 | |> decode.from(data) 420 | |> should.be_ok 421 | |> should.equal(option.None) 422 | } 423 | 424 | pub fn optional_error_test() { 425 | let data = dynamic.from(123) 426 | decode.optional(decode.string) 427 | |> decode.from(data) 428 | |> should.be_error 429 | |> should.equal([DecodeError("String", "Int", [])]) 430 | } 431 | 432 | pub fn map_test() { 433 | let data = dynamic.from(123) 434 | decode.int 435 | |> decode.map(int.to_string) 436 | |> decode.from(data) 437 | |> should.be_ok 438 | |> should.equal("123") 439 | } 440 | 441 | pub fn map_errors_test() { 442 | let data = dynamic.from(dict.from_list([#("data", 123)])) 443 | decode.at( 444 | ["data"], 445 | decode.map_errors(decode.string, fn(errors) { 446 | let assert [DecodeError("String", "Int", [])] = errors 447 | [ 448 | DecodeError("Wibble", "Wobble", ["ok"]), 449 | DecodeError("Wabble", "Wubble", ["ok"]), 450 | ] 451 | }), 452 | ) 453 | |> decode.from(data) 454 | |> should.be_error 455 | |> should.equal([ 456 | DecodeError("Wibble", "Wobble", ["data", "ok"]), 457 | DecodeError("Wabble", "Wubble", ["data", "ok"]), 458 | ]) 459 | } 460 | 461 | pub fn collapse_errors_test() { 462 | let data = dynamic.from(dict.from_list([#("data", 123)])) 463 | decode.at(["data"], decode.string |> decode.collapse_errors("Wibble")) 464 | |> decode.from(data) 465 | |> should.be_error 466 | |> should.equal([DecodeError("Wibble", "Int", ["data"])]) 467 | } 468 | 469 | pub fn then_test() { 470 | let decoder = 471 | decode.at(["key"], decode.int) 472 | |> decode.then(fn(i) { 473 | decode.at(["value"], case i { 474 | 1 -> decode.int |> decode.map(AnInt) 475 | _ -> decode.string |> decode.map(AString) 476 | }) 477 | }) 478 | 479 | decoder 480 | |> decode.from(dynamic.from(dict.from_list([#("key", 1), #("value", 100)]))) 481 | |> should.be_ok 482 | |> should.equal(AnInt(100)) 483 | 484 | decoder 485 | |> decode.from( 486 | dynamic.from( 487 | dict.from_list([ 488 | #("key", dynamic.from(2)), 489 | #("value", dynamic.from("Hi!")), 490 | ]), 491 | ), 492 | ) 493 | |> should.be_ok 494 | |> should.equal(AString("Hi!")) 495 | } 496 | 497 | type IntOrString { 498 | AnInt(Int) 499 | AString(String) 500 | } 501 | 502 | pub fn then_error_0_test() { 503 | let decoder = 504 | decode.at(["key"], decode.int) 505 | |> decode.then(fn(i) { 506 | decode.at(["value"], case i { 507 | 1 -> decode.int |> decode.map(AnInt) 508 | _ -> decode.string |> decode.map(AString) 509 | }) 510 | }) 511 | 512 | decoder 513 | |> decode.from(dynamic.from(123)) 514 | |> should.be_error 515 | |> should.equal([DecodeError("Dict", "Int", [])]) 516 | } 517 | 518 | pub fn then_error_1_test() { 519 | let decoder = 520 | decode.at(["key"], decode.int) 521 | |> decode.then(fn(i) { 522 | decode.at(["value"], case i { 523 | 1 -> decode.int |> decode.map(AnInt) 524 | _ -> decode.string |> decode.map(AString) 525 | }) 526 | }) 527 | 528 | decoder 529 | |> decode.from( 530 | dynamic.from( 531 | dict.from_list([ 532 | #("key", dynamic.from(1)), 533 | #("value", dynamic.from("Hi!")), 534 | ]), 535 | ), 536 | ) 537 | |> should.be_error 538 | |> should.equal([DecodeError("Int", "String", ["value"])]) 539 | } 540 | 541 | pub type MyEnum { 542 | A 543 | B 544 | C 545 | } 546 | 547 | pub fn then_enum_test() { 548 | let decoder = 549 | decode.string 550 | |> decode.then(fn(s) { 551 | case s { 552 | "a" -> decode.into(A) 553 | "b" -> decode.into(B) 554 | "c" -> decode.into(C) 555 | _ -> decode.fail("MyEnum") 556 | } 557 | }) 558 | 559 | decode.from(decoder, dynamic.from("a")) 560 | |> should.be_ok 561 | |> should.equal(A) 562 | 563 | decode.from(decoder, dynamic.from("b")) 564 | |> should.be_ok 565 | |> should.equal(B) 566 | 567 | decode.from(decoder, dynamic.from("c")) 568 | |> should.be_ok 569 | |> should.equal(C) 570 | 571 | decode.from(decoder, dynamic.from("d")) 572 | |> should.be_error 573 | |> should.equal([DecodeError("MyEnum", "String", [])]) 574 | } 575 | 576 | pub fn one_of_ok_0_test() { 577 | decode.one_of([ 578 | decode.string, 579 | decode.int 580 | |> decode.map(int.to_string), 581 | ]) 582 | |> decode.from(dynamic.from("Hello!")) 583 | |> should.be_ok 584 | |> should.equal("Hello!") 585 | } 586 | 587 | pub fn one_of_ok_1_test() { 588 | decode.one_of([ 589 | decode.string, 590 | decode.int 591 | |> decode.map(int.to_string), 592 | ]) 593 | |> decode.from(dynamic.from(123)) 594 | |> should.be_ok 595 | |> should.equal("123") 596 | } 597 | 598 | pub fn one_of_error_test() { 599 | decode.one_of([ 600 | decode.string, 601 | decode.int 602 | |> decode.map(int.to_string), 603 | ]) 604 | |> decode.from(dynamic.from(1.2)) 605 | |> should.be_error 606 | |> should.equal([DecodeError("Int", "Float", [])]) 607 | } 608 | 609 | pub fn fail_test() { 610 | decode.fail("WibbleWobble") 611 | |> decode.from(dynamic.from(123)) 612 | |> should.be_error 613 | |> should.equal([DecodeError("WibbleWobble", "Int", [])]) 614 | } 615 | -------------------------------------------------------------------------------- /test/decode_zero_test_ffi.mjs: -------------------------------------------------------------------------------- 1 | export function object(items) { 2 | const object = {}; 3 | for (const [k, v] of items) { 4 | object[k] = v; 5 | } 6 | return object; 7 | } 8 | 9 | export function map(items) { 10 | const object = new Map(); 11 | for (const [k, v] of items) { 12 | object.set(k, v); 13 | } 14 | return object; 15 | } 16 | --------------------------------------------------------------------------------