diff --git a/elm.json b/elm.json index 93127be..dbc76d0 100644 --- a/elm.json +++ b/elm.json @@ -12,5 +12,7 @@ "elm/core": "1.0.2 <= v < 2.0.0", "elm/json": "1.1.3 <= v < 2.0.0" }, - "test-dependencies": {} + "test-dependencies": { + "elm-explorations/test": "2.0.0 <= v < 3.0.0" + } } \ No newline at end of file diff --git a/src/DecodeComplete.elm b/src/DecodeComplete.elm index f7ea15e..38487e5 100644 --- a/src/DecodeComplete.elm +++ b/src/DecodeComplete.elm @@ -1,9 +1,14 @@ -module DecodeComplete exposing (ObjectDecoder, object, complete, required, optional, discard, discardOptional, hardcoded, discardRest, rest, restValues, andThen, fail) +module DecodeComplete exposing + ( ObjectDecoder, object + , required, optional, omissible, discard, discardOptional, hardcoded + , complete, discardRest, rest, restValues + , andThen, fail + ) {-| This module provides a way to decode JSON objects while making sure that all fields are handled. The interface works similar to json-decode-pipeline. For example, - import Json.Decode as D exposing (Decoder) import DecodeComplete exposing (..) + import Json.Decode as D exposing (Decoder) type alias User = { name : String @@ -20,24 +25,33 @@ module DecodeComplete exposing (ObjectDecoder, object, complete, required, optio decodes JSON objects that have precisely the fields `name`, `age`, and `email` and turns it into a `User` record, discarding the email address. -The general usage is as follows: Start decoding the object with `object f`, where `f` is the function being called with the results. Then decode the individual fields with `require`, `discard`, `optional`, `discardOptional`. At the end, turn turn the `ObjectDecoder` into a normal `Decoder` by calling `complete` (or `discardRest` or `rest` or `restValues`). +The general usage is as follows: Start decoding the object with `object f`, where `f` is the function being called with the results. Then decode the individual fields with `require`, `discard`, `optional`, `omissible`, `discardOptional`. At the end, turn turn the `ObjectDecoder` into a normal `Decoder` by calling `complete` (or `discardRest` or `rest` or `restValues`). + # Starting to decode + @docs ObjectDecoder, object + # Decoding fields -@docs required, optional, discard, discardOptional, hardcoded + +@docs required, optional, omissible, discard, discardOptional, hardcoded + # Finish decoding + @docs complete, discardRest, rest, restValues + # Special needs – decoding custom types and versioned data + @docs andThen, fail + -} import Dict exposing (Dict) -import Set exposing (Set) import Json.Decode as D exposing (Decoder) +import Set exposing (Set) {-| A decoder for JSON objects that makes sure that all fields in the JSON are handled @@ -70,6 +84,9 @@ required field decoder = {-| Decode the field given by the `String` parameter using the given (regular) `Decoder`. If the field is missing or the decoder fails, use the provided default value instead. + +If you want to not ignore field decoding failures, use `omissible`. + -} optional : String -> Decoder a -> a -> ObjectDecoder (a -> b) -> ObjectDecoder b optional field decoder default = @@ -81,6 +98,30 @@ optional field decoder default = ) +{-| Decode the field given by the `String` parameter using the given (regular) `Decoder`. If the field is missing use the provided default value instead. If the field decoder fails, the object decoder will fail. + +If you want to ignore field decoding failures, use `optional`. + +-} +omissible : String -> Decoder a -> a -> ObjectDecoder (a -> b) -> ObjectDecoder b +omissible field decoder default = + lift + (\( unhandled, f ) -> + D.maybe (D.field field D.value) + |> D.andThen + (\present -> + case present of + Just _ -> + D.map Just (D.field field decoder) + + Nothing -> + D.succeed Nothing + ) + |> D.map (Maybe.withDefault default) + |> D.map (\result -> ( Set.remove field unhandled, f result )) + ) + + {-| Require that a field is present, but discard its value. -} discard : String -> ObjectDecoder a -> ObjectDecoder a @@ -89,6 +130,7 @@ discard field = (\( unhandled, a ) -> if Set.member field unhandled then D.succeed ( Set.remove field unhandled, a ) + else D.fail ("Missing required discarded field `" ++ field ++ "`") ) @@ -117,6 +159,7 @@ complete (OD objectDecoder) = (\( unhandled, a ) -> if Set.isEmpty unhandled then D.succeed a + else D.fail ("The following fields where not handled: " ++ String.join ", " (Set.toList unhandled)) ) @@ -125,6 +168,7 @@ complete (OD objectDecoder) = {-| Turn the `ObjectDecoder` into a regular `Decoder`. Ignore if fields remain unhandled. This might be useful if you only want the check that all fields are handled to occur during development. You can use `complete` in development and change it into `discardRest` without having to change anything else. + -} discardRest : ObjectDecoder a -> Decoder a discardRest (OD objectDecoder) = @@ -135,7 +179,7 @@ discardRest (OD objectDecoder) = -} rest : Decoder a -> ObjectDecoder (Dict String a -> b) -> Decoder b rest aDecoder (OD objectDecoder) = - (objectDecoder + objectDecoder |> D.andThen (\( unhandled, f ) -> Set.foldl @@ -146,7 +190,6 @@ rest aDecoder (OD objectDecoder) = unhandled |> D.map f ) - ) {-| Finish up the `ObjectDecoder`, turning it into a regular decoder. Pass a dictionary of the unhandled fields (as `Decode.Value` values). @@ -183,6 +226,7 @@ restValues = |> complete first decodes the `version` field. If it is `0`, the JSON needs to have (exactly) the fields `name` and `age`. If the version is `1`, the JSON needs the fields `fullName`, `age` and `email` instead. If the version is anything else, fail. + -} andThen : (a -> ObjectDecoder b) -> ObjectDecoder a -> ObjectDecoder b andThen cont = diff --git a/tests/Tests.elm b/tests/Tests.elm new file mode 100644 index 0000000..d56cb10 --- /dev/null +++ b/tests/Tests.elm @@ -0,0 +1,51 @@ +module Tests exposing (..) + +import DecodeComplete +import Expect +import Json.Decode +import Test exposing (Test) + + +optionalTest : Test +optionalTest = + Test.describe "optional fields" + [ check "{}" (DecodeComplete.optional "a" Json.Decode.int -1) (Ok -1) + , check "{ \"a\" : 2 }" (DecodeComplete.optional "a" Json.Decode.int -1) (Ok 2) + , check "{ \"a\" : null }" (DecodeComplete.optional "a" Json.Decode.int -1) (Ok -1) + , check "{ \"a\" : 2.3 }" (DecodeComplete.optional "a" Json.Decode.int -1) (Ok -1) + ] + + +omissibleTest : Test +omissibleTest = + Test.describe "omissible fields" + [ check "{}" (DecodeComplete.omissible "a" Json.Decode.int -1) (Ok -1) + , check "{ \"a\" : 2 }" (DecodeComplete.omissible "a" Json.Decode.int -1) (Ok 2) + , check "{ \"a\" : null }" (DecodeComplete.omissible "a" Json.Decode.int -1) (Err ()) + , check "{ \"a\" : 2.3 }" (DecodeComplete.omissible "a" Json.Decode.int -1) (Err ()) + ] + + +check : + String + -> (DecodeComplete.ObjectDecoder (a -> a) -> DecodeComplete.ObjectDecoder a) + -> Result e a + -> Test +check input fieldDecoder result = + Test.test input <| + \_ -> + let + decoder : Json.Decode.Decoder a + decoder = + DecodeComplete.object identity + |> fieldDecoder + |> DecodeComplete.complete + in + case result of + Err _ -> + Json.Decode.decodeString decoder input + |> Expect.err + + Ok v -> + Json.Decode.decodeString decoder input + |> Expect.equal (Ok v)