├── .gitignore ├── README.md ├── elm-package.json ├── src └── Json │ ├── Bidirectional.elm │ └── Bidirectional │ └── Encoder.elm └── tests ├── .gitignore ├── Tests.elm └── elm-package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jamesmacaulay/elm-json-bidirectional 2 | 3 | This package lets you build up two-way `Coder` structures that concisely specify how values of some Elm type can be both encoded to and decoded from JSON. 4 | 5 | ## Why? 6 | 7 | If you frequently encode from and decode to the same Elm types, it can be tedious and error prone to define your encoders and decoders separately: 8 | 9 | ```elm 10 | import Json.Encode as Encode 11 | import Json.Decode as Decode exposing (Decoder) 12 | 13 | type alias User = 14 | { name : String 15 | , isAdmin : Bool 16 | } 17 | 18 | userDecoder : Decoder User 19 | userDecoder = 20 | Decode.map2 User 21 | (Decode.field "name" Decode.string) 22 | (Decode.field "isAdmin" Decode.bool) 23 | 24 | encodeUser : User -> Encode.Value 25 | encodeUser user = 26 | Encode.object 27 | [ ( "name", Encode.string user.name ) 28 | , ( "isAdmin", Encode.bool user.isAdmin ) 29 | ] 30 | ``` 31 | 32 | If you're encoding and decoding a lot of different kinds of data, this requires a lot of code in different functions that needs to be kept in sync. If you add a field to one of your record types but forget to add it to the type's encoder, the compiler can't help you find the omission and you might end up with bad data that can't be decoded with the corresponding decoder. [Fuzz tests work well to prevent this](https://www.brianthicks.com/post/2017/04/24/add-safety-to-your-elm-json-encoders-with-fuzz-testing/), but they require yet more code to be written to remedy the problem. 33 | 34 | With this package, you can instead build up a single `Coder` that knows how to both encode and decode: 35 | 36 | ```elm 37 | import Json.Bidirectional as Json 38 | 39 | type alias User = 40 | { name : String 41 | , isAdmin : Bool 42 | } 43 | 44 | userCoder : Json.Coder User 45 | userCoder = 46 | Json.object User 47 | |> Json.withField "name" .name Json.string 48 | |> Json.withField "isAdmin" .isAdmin Json.bool 49 | ``` 50 | 51 | Encoding and decoding is accomplished with the `encodeValue`, `encodeString`, `decodeValue`, and `decodeString` functions. If you want to get a Decoder from a Coder, you can use the `decoder` function. 52 | 53 | ## What's the catch? 54 | 55 | Because of the nature of the encoding and decoding processes, this approach is not so great if you are working with JSON that is of a very different structure than its corresponding Elm types. 56 | 57 | Also, specifying a bidirectional `Coder` for union types with more than one constructor is a bit of a hassle (see the `custom` function for an example). 58 | 59 | ## When should I use this package then? 60 | 61 | This package is at its best when you have full control over the shape of the JSON that you're encoding and decoding from. 62 | 63 | ## What about fuzz tests? 64 | 65 | Fuzz tests are a great way to make sure your encoders and decoders are mirror images of each other. Here's a great article on this topic that was also the inspiration for releasing this package: 66 | 67 | https://www.brianthicks.com/post/2017/04/24/add-safety-to-your-elm-json-encoders-with-fuzz-testing/ 68 | 69 | If you use this package to build bidirectional Coders, you won't need as many fuzz tests to ensure consistency, but you will still want them in some cases where it's possible to make mistakes. The Elm compiler will ensure that values encoded by a Coder will be able to be decoded to the original type by the same Coder, but the type system cannot always guarantee that the decoded _value_ will be identical to the original. Listed below are some ways that you can make asymmetrical Coders with this package if you aren't careful. These are situations where you might decide that fuzz tests are still worthwhile. 70 | 71 | ## Ordering object fields 72 | 73 | One way that the encoded and decoded values might not be equal is if you specify object fields out of order. For example: 74 | 75 | ```elm 76 | import Json.Bidirectional as Json 77 | 78 | type alias EmailContact = 79 | { name : String 80 | , email : String 81 | } 82 | 83 | emailContactCoder : Json.Coder EmailContact 84 | emailContactCoder = 85 | Json.object EmailContact 86 | -- fields in the wrong order! 87 | |> Json.withField "email" .email Json.string 88 | |> Json.withField "name" .name Json.string 89 | ``` 90 | 91 | The above Coder will encode `{ name = "Alice", email = "alice@example.com" }` correctly as `{"name": "Alice", "email": "alice@example.com"}`. However, because the two string fields are specified in the wrong order, the EmailContact constructor decodes the `"email"` field as its `name` and vice-versa. 92 | 93 | ## bimap 94 | 95 | The `bimap` function lets you map both the encoding and decoding processes of a Coder by supplying one function for each direction. Here's a contrived example: 96 | 97 | ```elm 98 | import Json.Bidirectional as Json 99 | 100 | type StringPair 101 | = StringPair String String 102 | 103 | stringPairCoder : Json.Coder StringPair 104 | stringPairCoder = 105 | Json.tuple (Json.string, Json.string) 106 | |> Json.bimap 107 | (\(StringPair left right) -> (left, right)) 108 | (\(left, right) -> StringPair left right) 109 | ``` 110 | 111 | These mapping functions are just complex enough that you might make a mistake in the implementation: 112 | 113 | ```elm 114 | inconsistentStringPairCoder : Json.Coder StringPair 115 | inconsistentStringPairCoder = 116 | Json.tuple (Json.string, Json.string) 117 | |> Json.bimap 118 | -- the left String is used in both places in the encoding! 119 | (\(StringPair left right) -> (left, left)) 120 | (\(left, right) -> StringPair left right) 121 | ``` 122 | 123 | ## custom 124 | 125 | The `custom` function lets you create an arbitrary Coder for any type by supplying an encoding function and Decoder for a single type. This function is most useful for implementing Coders for union types with multiple constructors. Use of the `custom` function in this way tends to be the most complex and error-prone way of constructing a Coder that this package makes available, and so fuzz testing `custom` Coders is highly recommended. 126 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "summary": "Two-way JSON encoder-decoders.", 4 | "repository": "https://github.com/jamesmacaulay/elm-json-bidirectional.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Json.Bidirectional" 11 | ], 12 | "dependencies": { 13 | "elm-lang/core": "5.1.1 <= v < 6.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /src/Json/Bidirectional.elm: -------------------------------------------------------------------------------- 1 | module Json.Bidirectional 2 | exposing 3 | ( Value 4 | , CoderBuilder 5 | , Coder 6 | , string 7 | , bool 8 | , int 9 | , float 10 | , nullable 11 | , list 12 | , object 13 | , withField 14 | , at 15 | , dict 16 | , keyValuePairs 17 | , tuple 18 | , tuple3 19 | , tuple4 20 | , tuple5 21 | , value 22 | , bimap 23 | , lazy 24 | , custom 25 | , encodeValue 26 | , encodeString 27 | , decodeValue 28 | , decodeString 29 | , decoder 30 | ) 31 | 32 | {-| The functions in this module let you build up two-way `Coder` structures that concisely specify how values of some Elm type can be both encoded to and decoded from JSON. 33 | 34 | # Types 35 | 36 | @docs Value, CoderBuilder, Coder 37 | 38 | # Primitives 39 | 40 | @docs string, bool, int, float 41 | 42 | # Data Structures 43 | 44 | @docs nullable, list, object, withField, at, dict, keyValuePairs, tuple, tuple3, tuple4, tuple5, value 45 | 46 | # Fancy Stuff 47 | 48 | @docs bimap, lazy, custom 49 | 50 | # Encoding and Decoding 51 | 52 | @docs encodeValue, encodeString, decodeValue, decodeString, decoder 53 | -} 54 | 55 | import Json.Decode as Decode exposing (Decoder) 56 | import Json.Encode as Encode 57 | import Json.Bidirectional.Encoder as Encoder exposing (Encoder) 58 | import Dict exposing (Dict) 59 | import Tuple 60 | 61 | 62 | {-| An alias for `Json.Encode.Value`. Can be used anywhere `Json.Encode.Value` or `Json.Decode.Value` is expected. 63 | -} 64 | type alias Value = 65 | Encode.Value 66 | 67 | 68 | {-| This type is used to represent Coders that are still being built, which means that the type they encode from may be different than the type they decode to. 69 | -} 70 | type CoderBuilder a b 71 | = CoderPair (Encoder a) (Decoder b) 72 | 73 | 74 | {-| An alias for `CoderBuilder a a`, this represents a symmetric pair of encoder and decoder. 75 | -} 76 | type alias Coder a = 77 | CoderBuilder a a 78 | 79 | 80 | {-| A Coder for String values in Elm, encoded as JSON strings. 81 | -} 82 | string : Coder String 83 | string = 84 | CoderPair 85 | (Encoder.opaque Encode.string) 86 | Decode.string 87 | 88 | 89 | {-| A Coder for Bool values in Elm, encoded as JSON true or false. 90 | -} 91 | bool : Coder Bool 92 | bool = 93 | CoderPair 94 | (Encoder.opaque Encode.bool) 95 | Decode.bool 96 | 97 | 98 | {-| A Coder for Int values in Elm, encoded as JSON numbers. 99 | -} 100 | int : Coder Int 101 | int = 102 | CoderPair 103 | (Encoder.opaque Encode.int) 104 | Decode.int 105 | 106 | 107 | {-| A Coder for Float values in Elm, encoded as JSON numbers. 108 | -} 109 | float : Coder Float 110 | float = 111 | CoderPair 112 | (Encoder.opaque Encode.float) 113 | Decode.float 114 | 115 | 116 | {-| Construct a Coder for Maybe values in Elm, where Nothing is encoded as null and Just values are encoded using the given Coder. 117 | -} 118 | nullable : Coder a -> Coder (Maybe a) 119 | nullable (CoderPair encoder decoder) = 120 | CoderPair 121 | (Encoder.nullable encoder) 122 | (Decode.nullable decoder) 123 | 124 | 125 | {-| Construct a Coder for List values in Elm, encoded as JSON arrays whose items are encoded as per the given Coder. 126 | -} 127 | list : Coder a -> Coder (List a) 128 | list (CoderPair encoder decoder) = 129 | CoderPair 130 | (Encoder.opaque (List.map (Encoder.encodeValue encoder) >> Encode.list)) 131 | (Decode.list decoder) 132 | 133 | 134 | {-| Begin construction of a Coder for values of an arbitrary Elm type, encoded as JSON objects with specific fields, each encoded in their own way: 135 | 136 | type alias User = 137 | { name : String 138 | , isAdmin : Bool 139 | } 140 | 141 | userCoder : Coder User 142 | userCoder = 143 | object User 144 | |> withField "name" .name string 145 | |> withField "isAdmin" .isAdmin bool 146 | 147 | The first argument is a constructor function that must take the values of the specified fields in order. Once all fields have been correctly specified by chaining `withField` calls in a pipeline, the types of the `CoderBuilder` converge and the result is a symmetric `Coder`. 148 | -} 149 | object : (a -> b) -> CoderBuilder c (a -> b) 150 | object constructor = 151 | CoderPair 152 | (Encoder.object []) 153 | (Decode.succeed constructor) 154 | 155 | 156 | {-| Adds a field to an object CoderBuilder pipeline. See `object` for an example. 157 | 158 | The first argument is the name of the field as it appears in the JSON. The second argument is a getter function that is used in the encoding process that takes a value of the source type and returns a value for the field. The third argument is a Coder for the field value. The fourth argument is the object CoderBuilder (usually applied via the `|>` operator) whose constructor function expects the field value as its next argument. Once all of the constructor function's arguments have been correctly applied, the types will converge and the result is a symmetric Coder. 159 | -} 160 | withField : 161 | String 162 | -> (a -> b) 163 | -> Coder b 164 | -> CoderBuilder a (b -> c) 165 | -> CoderBuilder a c 166 | withField name getter (CoderPair valueEncoder valueDecoder) (CoderPair objectEncoder objectDecoder) = 167 | let 168 | encoder = 169 | Encoder.withField name getter valueEncoder objectEncoder 170 | 171 | decoder = 172 | Decode.map2 (<|) objectDecoder (Decode.field name valueDecoder) 173 | in 174 | CoderPair encoder decoder 175 | 176 | 177 | {-| Transform a Coder such that its JSON representation is nested under the given list of object field names, from outermost to innermost. For example, 178 | 179 | fooBarIntCoder : Coder Int 180 | fooBarIntCoder = 181 | at [ "foo", "bar" ] int 182 | 183 | Encoding the Elm Int `33` with `fooBarIntCoder` produces the following JSON value: 184 | 185 | {"foo": {"bar": 33}} 186 | 187 | When decoding using the same Coder, the enclosed value is extracted from the specified nested fields. 188 | -} 189 | at : List String -> Coder a -> Coder a 190 | at keyPath (CoderPair encoder decoder) = 191 | CoderPair 192 | (Encoder.at keyPath encoder) 193 | (Decode.at keyPath decoder) 194 | 195 | 196 | {-| Construct a Coder for a Dict whose keys are Strings and whose values have the type of the given Coder argument. The Dict is encoded as a JSON object with an arbitrary list of fields, and each value encoded in the same way: 197 | 198 | dictJson : Value 199 | dictJson = 200 | [ ("foo", 1) 201 | , ("bar", 2) 202 | , ("baz", 3) 203 | ] 204 | |> Dict.fromList 205 | |> encodeValue (dict int) 206 | 207 | With the above code, `dictJson` has the following structure: 208 | 209 | {"foo": 1, "bar": 2, "baz": 3} 210 | 211 | Decoding is the same as with `Json.Decode.dict`. 212 | -} 213 | dict : Coder a -> Coder (Dict String a) 214 | dict (CoderPair encoder decoder) = 215 | CoderPair 216 | (Encoder.opaque 217 | (Dict.toList 218 | >> List.map (Tuple.mapSecond (Encoder.encodeValue encoder)) 219 | >> Encode.object 220 | ) 221 | ) 222 | (Decode.dict decoder) 223 | 224 | 225 | {-| Construct a Coder for a List of 2-tuples whose keys are Strings and whose values have the type of the given Coder argument. The List is encoded as a JSON object with an arbitrary list of fields, just like `dict`. 226 | 227 | Decoding is the same as with `Json.Decode.keyValuePairs`, while the encoding process uses `Json.Encode.Object`. This means that repeated use of the same key in the source list will result in a JSON object where the _last_ value from the repeated key ends up being used as that property's value. For example: 228 | 229 | twoFoos : Value 230 | twoFoos = 231 | [ ( "foo", 1 ), ( "foo", 2 ) ] 232 | |> encodeValue (keyValuePairs int) 233 | 234 | With the above code, `twoFoos` has the following structure: 235 | 236 | {"foo": 2} 237 | 238 | This behaviour means that if you aren't careful to keep your keys free of duplicates, then an encoding/decoding round-trip of the same Elm value could leave you with a value that isn't equal to the one you started with. 239 | -} 240 | keyValuePairs : Coder a -> Coder (List ( String, a )) 241 | keyValuePairs (CoderPair encoder decoder) = 242 | CoderPair 243 | (Encoder.opaque 244 | (List.map (Tuple.mapSecond (Encoder.encodeValue encoder)) 245 | >> Encode.object 246 | ) 247 | ) 248 | (Decode.keyValuePairs decoder) 249 | 250 | 251 | {-| Take a 2-tuple of Coders and produce a Coder of 2-tuples, encoding them as 2-element JSON arrays: 252 | 253 | intBoolTupleCoder : Coder ( Int, Bool ) 254 | intBoolTupleCoder = 255 | tuple (int, bool) 256 | 257 | tupleJson : Value 258 | tupleJson = 259 | ( 109, True ) 260 | |> encodeValue intBoolTupleCoder 261 | 262 | With the above code, `tupleJson` is the JSON array `[109, true]`. Decoding uses the specified value Coder for each index of the JSON array. 263 | -} 264 | tuple : ( Coder a, Coder b ) -> Coder ( a, b ) 265 | tuple ( CoderPair encA decA, CoderPair encB decB ) = 266 | CoderPair 267 | (Encoder.opaque 268 | (\( a, b ) -> 269 | Encode.list 270 | [ Encoder.encodeValue encA a 271 | , Encoder.encodeValue encB b 272 | ] 273 | ) 274 | ) 275 | (Decode.map2 (,) 276 | (Decode.index 0 decA) 277 | (Decode.index 1 decB) 278 | ) 279 | 280 | 281 | {-| Take a 3-tuple of Coders and produce a Coder of 3-tuples, encoding them as 3-element JSON arrays. See `tuple` for more details. 282 | -} 283 | tuple3 : ( Coder a, Coder b, Coder c ) -> Coder ( a, b, c ) 284 | tuple3 ( CoderPair encA decA, CoderPair encB decB, CoderPair encC decC ) = 285 | CoderPair 286 | (Encoder.opaque 287 | (\( a, b, c ) -> 288 | Encode.list 289 | [ Encoder.encodeValue encA a 290 | , Encoder.encodeValue encB b 291 | , Encoder.encodeValue encC c 292 | ] 293 | ) 294 | ) 295 | (Decode.map3 (,,) 296 | (Decode.index 0 decA) 297 | (Decode.index 1 decB) 298 | (Decode.index 2 decC) 299 | ) 300 | 301 | 302 | {-| Take a 4-tuple of Coders and produce a Coder of 4-tuples, encoding them as 4-element JSON arrays. See `tuple` for more details. 303 | -} 304 | tuple4 : ( Coder a, Coder b, Coder c, Coder d ) -> Coder ( a, b, c, d ) 305 | tuple4 ( CoderPair encA decA, CoderPair encB decB, CoderPair encC decC, CoderPair encD decD ) = 306 | CoderPair 307 | (Encoder.opaque 308 | (\( a, b, c, d ) -> 309 | Encode.list 310 | [ Encoder.encodeValue encA a 311 | , Encoder.encodeValue encB b 312 | , Encoder.encodeValue encC c 313 | , Encoder.encodeValue encD d 314 | ] 315 | ) 316 | ) 317 | (Decode.map4 (,,,) 318 | (Decode.index 0 decA) 319 | (Decode.index 1 decB) 320 | (Decode.index 2 decC) 321 | (Decode.index 3 decD) 322 | ) 323 | 324 | 325 | {-| Take a 5-tuple of Coders and produce a Coder of 5-tuples, encoding them as 5-element JSON arrays. See `tuple` for more details. 326 | -} 327 | tuple5 : ( Coder a, Coder b, Coder c, Coder d, Coder e ) -> Coder ( a, b, c, d, e ) 328 | tuple5 ( CoderPair encA decA, CoderPair encB decB, CoderPair encC decC, CoderPair encD decD, CoderPair encE decE ) = 329 | CoderPair 330 | (Encoder.opaque 331 | (\( a, b, c, d, e ) -> 332 | Encode.list 333 | [ Encoder.encodeValue encA a 334 | , Encoder.encodeValue encB b 335 | , Encoder.encodeValue encC c 336 | , Encoder.encodeValue encD d 337 | , Encoder.encodeValue encE e 338 | ] 339 | ) 340 | ) 341 | (Decode.map5 (,,,,) 342 | (Decode.index 0 decA) 343 | (Decode.index 1 decB) 344 | (Decode.index 2 decC) 345 | (Decode.index 3 decD) 346 | (Decode.index 4 decE) 347 | ) 348 | 349 | 350 | {-| A Coder for arbitrary JSON values that are left untouched in both the encoding and decoding processes. 351 | -} 352 | value : Coder Value 353 | value = 354 | CoderPair (Encoder.opaque identity) Decode.value 355 | 356 | 357 | {-| Map both the encoding and decoding processes of a Coder, producing a new Coder of a new type. The first argument is a function that maps over the encoding process, taking Elm values of type `b` and producing `a` values that the given `Coder a` knows how to encode. The second argument is a function that maps over the decoding process, taking results from the given `Coder a` and transforming them into the ones that are produced by the resulting `Coder b`. For example: 358 | 359 | setCoder : Coder Set 360 | setCoder = 361 | list 362 | |> bimap Set.toList Set.fromList 363 | -} 364 | bimap : (b -> a) -> (a -> b) -> Coder a -> Coder b 365 | bimap ba ab (CoderPair encoder decoder) = 366 | CoderPair 367 | (Encoder.opaque (ba >> Encoder.encodeValue encoder)) 368 | (Decode.map ab decoder) 369 | 370 | 371 | {-| This function makes it possible to define Coders for recursive JSON structures. Use it like you use `Json.Decode.lazy`: 372 | 373 | type alias Comment = 374 | { message : String 375 | , responses : Responses 376 | } 377 | 378 | type Responses = Responses (List Comment) 379 | 380 | responses : Coder Responses 381 | responses = 382 | bimap 383 | (\(Responses comments) -> comments) 384 | Responses 385 | (list (lazy (\_ -> comment))) 386 | 387 | comment : Coder Comment 388 | comment = 389 | object Comment 390 | |> withField "message" .message string 391 | |> withField "responses" .responses responses 392 | -} 393 | lazy : (() -> Coder a) -> Coder a 394 | lazy getCoder = 395 | CoderPair 396 | (Encoder.opaque (\x -> encodeValue (getCoder ()) x)) 397 | (Decode.andThen (getCoder >> decoder) (Decode.succeed ())) 398 | 399 | 400 | {-| Construct a custom Coder out of an encoding function and a Decoder for the same Elm type. This is useful for defining Coders of union types: 401 | 402 | 403 | resultCoder : Coder err -> Coder ok -> Coder (Result err ok) 404 | resultCoder errCoder okCoder = 405 | let 406 | wrappedErr = 407 | at [ "err" ] errCoder 408 | 409 | wrappedOk = 410 | at [ "ok" ] okCoder 411 | in 412 | custom 413 | (\result -> 414 | case result of 415 | Err x -> 416 | encodeValue wrappedErr x 417 | 418 | Ok x -> 419 | encodeValue wrappedOk x 420 | ) 421 | (Decode.oneOf 422 | [ Decode.map Err (decoder wrappedErr) 423 | , Decode.map Ok (decoder wrappedOk) 424 | ] 425 | ) 426 | -} 427 | custom : (a -> Value) -> Decoder a -> Coder a 428 | custom toValue decoder = 429 | CoderPair (Encoder.opaque toValue) decoder 430 | 431 | 432 | {-| Use a Coder to encode something to JSON as a Value, which can be used anywhere that `Json.Encode.Value` or `Json.Decode.Value` is expected. 433 | -} 434 | encodeValue : Coder a -> a -> Value 435 | encodeValue (CoderPair encoder _) = 436 | Encoder.encodeValue encoder 437 | 438 | 439 | {-| Use a Coder to encode something to JSON as a String. The second argument is the amount of indentation to use for serialization, with `0` resulting in a one-line JSON String. 440 | -} 441 | encodeString : Coder a -> Int -> a -> String 442 | encodeString coder indentation = 443 | encodeValue coder >> Encode.encode indentation 444 | 445 | 446 | {-| Use a Coder to decode a JSON Value into the Coder's type. Returns a Result with String error messages because the structure of the JSON may not match what is expected by the Coder. 447 | -} 448 | decodeValue : Coder a -> Value -> Result String a 449 | decodeValue (CoderPair _ decoder) = 450 | Decode.decodeValue decoder 451 | 452 | 453 | {-| Use a Coder to decode a JSON String into the Coder's type. Returns a Result with String error messages because the structure of the JSON may not match what is expected by the Coder. 454 | -} 455 | decodeString : Coder a -> String -> Result String a 456 | decodeString (CoderPair _ decoder) = 457 | Decode.decodeString decoder 458 | 459 | 460 | {-| Get a `Json.Decode.Decoder` from a `Coder`. 461 | -} 462 | decoder : Coder a -> Decoder a 463 | decoder (CoderPair _ decoder) = 464 | decoder 465 | -------------------------------------------------------------------------------- /src/Json/Bidirectional/Encoder.elm: -------------------------------------------------------------------------------- 1 | module Json.Bidirectional.Encoder 2 | exposing 3 | ( Encoder 4 | , opaque 5 | , object 6 | , nullable 7 | , withField 8 | , encodeValue 9 | , contramap 10 | , at 11 | ) 12 | 13 | import Json.Encode as Encode exposing (Value) 14 | 15 | 16 | type Encoder a 17 | = Opaque (a -> Value) 18 | | Object (List ( String, a -> Value )) 19 | 20 | 21 | opaque : (a -> Value) -> Encoder a 22 | opaque = 23 | Opaque 24 | 25 | 26 | object : List ( String, a -> Value ) -> Encoder a 27 | object = 28 | Object 29 | 30 | 31 | maybeValue : Encoder a -> Maybe a -> Value 32 | maybeValue encoder maybeX = 33 | case maybeX of 34 | Nothing -> 35 | Encode.null 36 | 37 | Just x -> 38 | encodeValue encoder x 39 | 40 | 41 | nullable : Encoder a -> Encoder (Maybe a) 42 | nullable encoder = 43 | opaque (maybeValue encoder) 44 | 45 | 46 | withField : String -> (a -> b) -> Encoder b -> Encoder a -> Encoder a 47 | withField name getter valueEncoder objectEncoder = 48 | case objectEncoder of 49 | Object toValuePairs -> 50 | Object (toValuePairs ++ [ ( name, getter >> encodeValue valueEncoder ) ]) 51 | 52 | Opaque _ -> 53 | objectEncoder 54 | 55 | 56 | encodeValue : Encoder a -> a -> Value 57 | encodeValue encoder x = 58 | case encoder of 59 | Opaque toValue -> 60 | toValue x 61 | 62 | Object toValuePairs -> 63 | toValuePairs 64 | |> List.map (Tuple.mapSecond (\toValue -> toValue x)) 65 | |> Encode.object 66 | 67 | 68 | contramap : (a -> b) -> Encoder b -> Encoder a 69 | contramap f encoder = 70 | case encoder of 71 | Opaque toValue -> 72 | Opaque (f >> toValue) 73 | 74 | Object toValuePairs -> 75 | toValuePairs 76 | |> List.map (Tuple.mapSecond (\toValue -> f >> toValue)) 77 | |> Object 78 | 79 | 80 | at : List String -> Encoder a -> Encoder a 81 | at keyPath encoder = 82 | case keyPath of 83 | [] -> 84 | encoder 85 | 86 | key :: rest -> 87 | object [ ( key, encodeValue (at rest encoder) ) ] 88 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (..) 2 | 3 | import Test exposing (Test) 4 | import Expect 5 | import Fuzz exposing (Fuzzer) 6 | import Json.Bidirectional as Json 7 | import Json.Decode as Decode 8 | import Json.Encode as Encode 9 | import Dict exposing (Dict) 10 | 11 | 12 | testFuzzedCoderRoundTrip : String -> Fuzzer a -> Fuzzer (Json.Coder a) -> Test 13 | testFuzzedCoderRoundTrip description valueFuzzer coderFuzzer = 14 | Test.fuzz (Fuzz.map2 (,) valueFuzzer coderFuzzer) description <| 15 | \( value, coder ) -> 16 | value 17 | |> Json.encodeValue coder 18 | |> Json.decodeValue coder 19 | |> Expect.equal (Ok value) 20 | 21 | 22 | testCoderRoundTrip : String -> Fuzzer a -> Json.Coder a -> Test 23 | testCoderRoundTrip description fuzzer coder = 24 | testFuzzedCoderRoundTrip description fuzzer (Fuzz.constant coder) 25 | 26 | 27 | type alias Thing = 28 | { a : String 29 | , b : Bool 30 | , c : Int 31 | } 32 | 33 | 34 | thingCoder : Json.Coder Thing 35 | thingCoder = 36 | Json.object Thing 37 | |> Json.withField "a" .a Json.string 38 | |> Json.withField "b" .b Json.bool 39 | |> Json.withField "c" .c Json.int 40 | 41 | 42 | type BinaryTree a 43 | = Leaf a 44 | | Branch (BinaryTree a) (BinaryTree a) 45 | 46 | 47 | binaryTreeFuzzer : Fuzz.Fuzzer a -> Fuzz.Fuzzer (BinaryTree a) 48 | binaryTreeFuzzer valueFuzzer = 49 | let 50 | go maxDepth = 51 | if maxDepth <= 0 then 52 | Fuzz.map Leaf valueFuzzer 53 | else 54 | let 55 | recurse = 56 | go (maxDepth - 1) 57 | in 58 | Fuzz.frequency 59 | [ ( 1, Fuzz.map Leaf valueFuzzer ) 60 | , ( 2, Fuzz.map2 Branch recurse recurse ) 61 | ] 62 | in 63 | go 2 64 | 65 | 66 | leafCoder : Json.Coder a -> Json.Coder a 67 | leafCoder valueCoder = 68 | Json.at [ "leaf" ] valueCoder 69 | 70 | 71 | branchCoder : Json.Coder a -> Json.Coder ( BinaryTree a, BinaryTree a ) 72 | branchCoder valueCoder = 73 | let 74 | node = 75 | Json.lazy (\_ -> binaryTreeCoder valueCoder) 76 | in 77 | Json.at [ "branch" ] (Json.tuple ( node, node )) 78 | 79 | 80 | binaryTreeencodeValue : Json.Coder a -> BinaryTree a -> Json.Value 81 | binaryTreeencodeValue valueCoder node = 82 | case node of 83 | Leaf value -> 84 | Json.encodeValue (leafCoder valueCoder) value 85 | 86 | Branch left right -> 87 | Json.encodeValue (branchCoder valueCoder) ( left, right ) 88 | 89 | 90 | binaryTreeDecoder : Json.Coder a -> Decode.Decoder (BinaryTree a) 91 | binaryTreeDecoder valueCoder = 92 | Decode.oneOf 93 | [ Decode.map Leaf (Json.decoder (leafCoder valueCoder)) 94 | , Decode.map (uncurry Branch) (Json.decoder (branchCoder valueCoder)) 95 | ] 96 | 97 | 98 | binaryTreeCoder : Json.Coder a -> Json.Coder (BinaryTree a) 99 | binaryTreeCoder valueCoder = 100 | Json.custom 101 | (binaryTreeencodeValue valueCoder) 102 | (binaryTreeDecoder valueCoder) 103 | 104 | 105 | dictFuzzer : Fuzzer a -> Fuzzer (Dict String a) 106 | dictFuzzer valueFuzzer = 107 | Fuzz.map2 (,) Fuzz.string valueFuzzer 108 | |> Fuzz.list 109 | |> Fuzz.map Dict.fromList 110 | 111 | 112 | resultCoder : Json.Coder err -> Json.Coder ok -> Json.Coder (Result err ok) 113 | resultCoder errCoder okCoder = 114 | let 115 | wrappedErr = 116 | Json.at [ "err" ] errCoder 117 | 118 | wrappedOk = 119 | Json.at [ "ok" ] okCoder 120 | in 121 | Json.custom 122 | (\result -> 123 | case result of 124 | Err x -> 125 | Json.encodeValue wrappedErr x 126 | 127 | Ok x -> 128 | Json.encodeValue wrappedOk x 129 | ) 130 | (Decode.oneOf 131 | [ Decode.map Err (Json.decoder wrappedErr) 132 | , Decode.map Ok (Json.decoder wrappedOk) 133 | ] 134 | ) 135 | 136 | 137 | type Constant a 138 | = Constant a 139 | 140 | 141 | all : Test 142 | all = 143 | Test.describe "Json.Bidirectional" 144 | [ Test.describe "Fuzz tests" 145 | [ testCoderRoundTrip "string" 146 | Fuzz.string 147 | Json.string 148 | , testCoderRoundTrip "bool" 149 | Fuzz.bool 150 | Json.bool 151 | , testCoderRoundTrip "int" 152 | Fuzz.int 153 | Json.int 154 | , testCoderRoundTrip "float" 155 | Fuzz.float 156 | Json.float 157 | , testCoderRoundTrip "nullable" 158 | (Fuzz.maybe Fuzz.int) 159 | (Json.nullable Json.int) 160 | , testCoderRoundTrip "list" 161 | (Fuzz.list Fuzz.int) 162 | (Json.list Json.int) 163 | , testCoderRoundTrip "object ... |> withField ... |> withField ..." 164 | (Fuzz.map3 Thing Fuzz.string Fuzz.bool Fuzz.int) 165 | thingCoder 166 | , testFuzzedCoderRoundTrip "at" 167 | Fuzz.int 168 | (Fuzz.list Fuzz.string 169 | |> Fuzz.map (\keyPath -> Json.at keyPath Json.int) 170 | ) 171 | , testCoderRoundTrip "dict" 172 | (dictFuzzer Fuzz.int) 173 | (Json.dict Json.int) 174 | , Test.fuzz (Fuzz.list (Fuzz.tuple ( Fuzz.string, Fuzz.int ))) "keyValuePairs" <| 175 | \pairs -> 176 | let 177 | sortedDedupedPairs = 178 | pairs 179 | |> Dict.fromList 180 | |> Dict.toList 181 | |> List.sort 182 | 183 | coder = 184 | Json.keyValuePairs Json.int 185 | in 186 | sortedDedupedPairs 187 | |> Json.encodeValue coder 188 | |> Json.decodeValue coder 189 | |> Result.map List.sort 190 | |> Expect.equal (Ok sortedDedupedPairs) 191 | , Test.fuzz3 Fuzz.string Fuzz.int Fuzz.int "keyValuePairs repeated key takes last value" <| 192 | \k v1 v2 -> 193 | let 194 | coder = 195 | Json.keyValuePairs Json.int 196 | in 197 | [ ( k, v1 ), ( k, v2 ) ] 198 | |> Json.encodeValue coder 199 | |> Json.decodeValue coder 200 | |> Expect.equal (Ok [ ( k, v2 ) ]) 201 | , testCoderRoundTrip "tuple" 202 | (Fuzz.tuple ( Fuzz.string, Fuzz.bool )) 203 | (Json.tuple ( Json.string, Json.bool )) 204 | , testCoderRoundTrip "tuple3" 205 | (Fuzz.tuple3 ( Fuzz.string, Fuzz.bool, Fuzz.int )) 206 | (Json.tuple3 ( Json.string, Json.bool, Json.int )) 207 | , testCoderRoundTrip "tuple4" 208 | (Fuzz.tuple4 ( Fuzz.string, Fuzz.bool, Fuzz.int, Fuzz.float )) 209 | (Json.tuple4 ( Json.string, Json.bool, Json.int, Json.float )) 210 | , testCoderRoundTrip "tuple5" 211 | (Fuzz.tuple5 212 | ( Fuzz.string 213 | , Fuzz.bool 214 | , Fuzz.int 215 | , Fuzz.float 216 | , (dictFuzzer Fuzz.int) 217 | ) 218 | ) 219 | (Json.tuple5 220 | ( Json.string 221 | , Json.bool 222 | , Json.int 223 | , Json.float 224 | , (Json.dict Json.int) 225 | ) 226 | ) 227 | , testCoderRoundTrip "BinaryTree coder using custom and lazy" 228 | (binaryTreeFuzzer Fuzz.int) 229 | (binaryTreeCoder Json.int) 230 | , testCoderRoundTrip "value" 231 | (Fuzz.map Encode.string Fuzz.string) 232 | Json.value 233 | , testCoderRoundTrip "resultCoder" 234 | (Fuzz.result Fuzz.string Fuzz.int) 235 | (resultCoder Json.string Json.int) 236 | , testCoderRoundTrip "bimap" 237 | (Fuzz.map Constant Fuzz.int) 238 | (Json.bimap (\(Constant x) -> x) Constant Json.int) 239 | ] 240 | ] 241 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Sample Elm Test", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | --------------------------------------------------------------------------------