├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── elm.json ├── LICENSE ├── README.md ├── examples └── Example.elm ├── FAQ.md ├── tests ├── Advanced.elm ├── Forward.elm └── Base.elm └── src ├── Codec └── Advanced.elm └── Codec.elm /.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elm 2 | sudo: false -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - First version 10 | 11 | [Unreleased]: https://github.com/miniBill/elm-codec/compare/v1.0.0...HEAD 12 | [1.0.0]: https://github.com/miniBill/elm-codec/releases/tag/v1.0.0 -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "miniBill/elm-codec", 4 | "summary": "Build JSON encoders and decoders with minimal boilerplate", 5 | "license": "MIT", 6 | "version": "2.0.0", 7 | "exposed-modules": [ 8 | "Codec", 9 | "Codec.Advanced" 10 | ], 11 | "elm-version": "0.19.0 <= v < 0.20.0", 12 | "dependencies": { 13 | "elm/core": "1.0.0 <= v < 2.0.0", 14 | "elm/json": "1.0.0 <= v < 2.0.0" 15 | }, 16 | "test-dependencies": { 17 | "elm/random": "1.0.0 <= v < 2.0.0", 18 | "elm-explorations/test": "1.2.0 <= v < 2.0.0" 19 | } 20 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Leonardo Taglialegne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-codecs 2 | 3 | [![Build Status](https://travis-ci.org/miniBill/elm-codec.svg?branch=master)](https://travis-ci.org/miniBill/elm-codec) 4 | 5 | This package allows you to build pairs of JSON encoders (`a -> Value`) and decoders (`Decoder a`), collectively called a `Codec a`. 6 | 7 | It supports all of the basic types, collections, records and even custom types! See an example at the bottom of this document. 8 | 9 | ## Design Goals 10 | 11 | The design goal is to be as type safe as possible while keeping a nice API. 12 | Using this package will greatly reduce the risk of unmatched encoders and decoders. 13 | 14 | The packages re-exposes the `Value` and `Decoder` types from `elm/json`, so you don't need to import them too. 15 | 16 | ## Learning Resources 17 | 18 | Ask for help on the [Elm Slack](https://elmlang.herokuapp.com/). 19 | 20 | You can also have a look at the `FAQ.md` file. 21 | 22 | ## Examples 23 | See the `examples` folder for more examples. 24 | 25 | ### Basic usage ### 26 | 27 | ```elm 28 | import Codec exposing (Codec, Value) 29 | 30 | codec : Codec (List Int) 31 | codec = 32 | Codec.list Codec.int 33 | 34 | encode : List Int -> Value 35 | encode list = 36 | Codec.encoder codec list 37 | 38 | decodeString : String -> Result Codec.Error (List Int) 39 | decodeString s = 40 | Codec.decodeString codec s 41 | ``` 42 | 43 | ### Custom types ### 44 | 45 | ```elm 46 | type Semaphore 47 | = Red Int String 48 | | Yellow 49 | | Green Float 50 | 51 | semaphoreCodec : Codec Semaphore 52 | semaphoreCodec = 53 | Codec.custom 54 | (\red yellow green value -> 55 | case value of 56 | Red i s -> 57 | red i s 58 | 59 | Yellow -> 60 | yellow 61 | 62 | Green f -> 63 | green f 64 | ) 65 | |> Codec.variant2 "Red" Red Codec.int Codec.string 66 | |> Codec.variant0 "Yellow" Yellow 67 | |> Codec.variant1 "Green" Green Codec.float 68 | |> Codec.buildCustom 69 | ``` 70 | -------------------------------------------------------------------------------- /examples/Example.elm: -------------------------------------------------------------------------------- 1 | module Examples exposing (Point, Tree(..), pointCodec, treeCodec) 2 | 3 | import Codec exposing (Codec) 4 | 5 | 6 | type alias Point = 7 | { x : Int 8 | , y : Int 9 | } 10 | 11 | 12 | pointCodec : Codec Point 13 | pointCodec = 14 | Codec.object Point 15 | |> Codec.field "x" .x Codec.int 16 | |> Codec.field "y" .y Codec.int 17 | |> Codec.buildObject 18 | 19 | 20 | type Peano 21 | = Peano (Maybe Peano) 22 | 23 | 24 | peanoCodec : Codec Peano 25 | peanoCodec = 26 | Codec.recursive 27 | (\finishedCodec -> 28 | Codec.maybe finishedCodec 29 | |> Codec.map Peano (\(Peano p) -> p) 30 | ) 31 | 32 | 33 | type Semaphore 34 | = Red Int String 35 | | Yellow Float 36 | | Green 37 | 38 | 39 | semaphoreCodec : Codec Semaphore 40 | semaphoreCodec = 41 | Codec.custom 42 | (\red yellow green value -> 43 | case value of 44 | Red i s -> 45 | red i s 46 | 47 | Yellow f -> 48 | yellow f 49 | 50 | Green -> 51 | green 52 | ) 53 | |> Codec.variant2 "Red" Red Codec.int Codec.string 54 | |> Codec.variant1 "Yellow" Yellow Codec.float 55 | |> Codec.variant0 "Green" Green 56 | |> Codec.buildCustom 57 | 58 | 59 | type Tree a 60 | = Node (List (Tree a)) 61 | | Leaf a 62 | 63 | 64 | treeCodec : Codec a -> Codec (Tree a) 65 | treeCodec meta = 66 | Codec.recursive 67 | (\rmeta -> 68 | let 69 | match fnode fleaf tree = 70 | case tree of 71 | Node cs -> 72 | fnode cs 73 | 74 | Leaf x -> 75 | fleaf x 76 | in 77 | Codec.custom match 78 | |> Codec.variant1 "Node" Node (Codec.list rmeta) 79 | |> Codec.variant1 "Leaf" Leaf meta 80 | |> Codec.buildCustom 81 | ) 82 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | ## How do you use `recursive`? 2 | The trick to understanding the `recursive` codec is: pretend you are already done. 3 | When the function you pass to `recursive` is called the argument is the finished `Codec`. 4 | 5 | An example may be worth a thousand words: 6 | 7 | ```elm 8 | type Peano 9 | = Peano (Maybe Peano) 10 | 11 | 12 | peanoCodec : Codec Peano 13 | peanoCodec = 14 | Codec.recursive 15 | (\finishedCodec -> 16 | Codec.maybe finishedCodec 17 | |> Codec.map Peano (\(Peano p) -> p) 18 | ) 19 | ``` 20 | 21 | ## Why does `map` take two opposite functions? 22 | One is used for the encoder, the other for the decoder 23 | 24 | ## How do I build `Codec`s for custom types? 25 | You start building with `custom` which needs the pattern matcher for your type as an argument. 26 | 27 | The pattern matcher is just the most generic `case ... of` possible for your type. 28 | 29 | You then chain `variantX` calls for every alternative (in the same order as the pattern matcher). 30 | 31 | You end with a call to `buildCustom`. 32 | 33 | An example: 34 | 35 | ```elm 36 | type Semaphore 37 | = Red Int String 38 | | Yellow Float 39 | | Green 40 | 41 | 42 | semaphoreCodec : Codec Semaphore 43 | semaphoreCodec = 44 | Codec.custom 45 | (\fred fyellow fgreen value -> 46 | case value of 47 | Red i s -> 48 | fred i s 49 | 50 | Yellow f -> 51 | fyellow f 52 | 53 | Green -> 54 | fgreen 55 | ) 56 | |> Codec.variant2 "Red" Red Codec.int Codec.string 57 | |> Codec.variant1 "Yellow" Yellow Codec.float 58 | |> Codec.variant0 "Green" Green 59 | |> Codec.buildCustom 60 | ``` 61 | 62 | ## What happens to existing `Value`s if I change my `Codec`s? 63 | Old `Value`s will be parsed fine by new `Codec`s if you: 64 | * add new `variant`s to custom types, 65 | * remove (from the end) parameters from `variant`s, 66 | * change any `Codec` to a `succeed` one or 67 | * add optional fields (`maybeField`) to records. 68 | 69 | New `Value`s will be parsed fine by old `Codec`s if you: 70 | * remove `variant`s from custom types, 71 | * append parameters to `variant`s , 72 | * change a `succeed` `Codec` to any other one or 73 | * remove fields from records. 74 | -------------------------------------------------------------------------------- /tests/Advanced.elm: -------------------------------------------------------------------------------- 1 | module Advanced exposing (..) 2 | 3 | import Codec exposing (Codec) 4 | import Codec.Advanced as Codec 5 | import Expect 6 | import Fuzz exposing (Fuzzer) 7 | import Json.Encode as JE 8 | import Test exposing (Test, describe, fuzz) 9 | 10 | 11 | type Semaphore 12 | = Red Int String 13 | | Yellow 14 | | Green Float 15 | 16 | 17 | semaphoreCodec : Codec Semaphore 18 | semaphoreCodec = 19 | Codec.customObject "color" 20 | (\red yellow green value -> 21 | case value of 22 | Red i s -> 23 | red i s 24 | 25 | Yellow -> 26 | yellow 27 | 28 | Green f -> 29 | green f 30 | ) 31 | |> Codec.objectVariant2 "red" Red ( "first", Codec.int ) ( "second", Codec.string ) 32 | |> Codec.objectVariant0 "yellow" Yellow 33 | |> Codec.objectVariant1 "green" Green ( "value", Codec.float ) 34 | |> Codec.buildCustomObject 35 | 36 | 37 | suite : Test 38 | suite = 39 | describe "Testing customObjectCodec" 40 | [ describe "Roundtrip" [ roundtripTest ] 41 | , describe "Correct shapes" shapesTests 42 | ] 43 | 44 | 45 | roundtripTest : Test 46 | roundtripTest = 47 | fuzz semaphoreFuzzer "is a roundtrip" <| \value -> 48 | value 49 | |> Codec.encoder semaphoreCodec 50 | |> Codec.decodeValue semaphoreCodec 51 | |> Expect.equal (Ok value) 52 | 53 | 54 | semaphoreFuzzer : Fuzzer Semaphore 55 | semaphoreFuzzer = 56 | Fuzz.oneOf 57 | [ Fuzz.map2 Red Fuzz.int Fuzz.string 58 | , Fuzz.constant Yellow 59 | , Fuzz.map Green Fuzz.float 60 | ] 61 | 62 | 63 | shapesTests : List Test 64 | shapesTests = 65 | [ ( "Red decode" 66 | , [ ( "color", JE.string "red" ) 67 | , ( "first", JE.int 42 ) 68 | , ( "second", JE.string "413" ) 69 | ] 70 | , Ok <| Red 42 "413" 71 | ) 72 | , ( "Yellow decode" 73 | , [ ( "color", JE.string "yellow" ) 74 | , ( "extra", JE.null ) 75 | ] 76 | , Ok Yellow 77 | ) 78 | , ( "Green decode" 79 | , [ ( "color", JE.string "green" ) 80 | , ( "value", JE.float -42 ) 81 | ] 82 | , Ok <| Green -42 83 | ) 84 | , ( "Missing fields fail" 85 | , [ ( "color", JE.string "green" ) ] 86 | , Codec.decodeString (Codec.fail "Expecting an OBJECT with a field named `value`") """{"color": "green"}""" 87 | ) 88 | , ( "Wrong tag fail" 89 | , [ ( "color", JE.string "gray" ) ] 90 | , Codec.decodeString (Codec.fail "color \"gray\" did not match") """{"color": "gray"}""" 91 | ) 92 | ] 93 | |> List.map 94 | (\( name, fields, expected ) -> 95 | Test.test name <| \_ -> 96 | fields 97 | |> JE.object 98 | |> Codec.decodeValue semaphoreCodec 99 | |> Expect.equal expected 100 | ) 101 | -------------------------------------------------------------------------------- /tests/Forward.elm: -------------------------------------------------------------------------------- 1 | module Forward exposing (suite) 2 | 3 | import Base 4 | import Codec exposing (Codec) 5 | import Expect 6 | import Fuzz exposing (Fuzzer) 7 | import Json.Decode as JD 8 | import Test exposing (Test, describe, fuzz) 9 | 10 | 11 | suite : Test 12 | suite = 13 | describe "Testing forward and backward compat" 14 | [ -- describe "Adding a variant" addVariant 15 | --, describe "Remove parameters" removeParameters 16 | describe "Any to succeed" anyToSucceed 17 | , describe "Add optional field" addMaybeField 18 | ] 19 | 20 | 21 | compatible : Fuzzer a -> (a -> b) -> Codec a -> Codec b -> Test 22 | compatible fuzzer map oldCodec newCodec = 23 | fuzz fuzzer "compatible" <| 24 | \value -> 25 | value 26 | |> Codec.encoder oldCodec 27 | |> JD.decodeValue (Codec.decoder newCodec) 28 | |> Expect.equal (Ok <| map value) 29 | 30 | 31 | forward : Fuzzer old -> (old -> new) -> Codec old -> Codec new -> Test 32 | forward fuzzer map oldCodec newCodec = 33 | describe "forward" 34 | [ describe "old" 35 | [ Base.roundtrips fuzzer oldCodec 36 | ] 37 | , describe "new" 38 | [ Base.roundtrips (Fuzz.map map fuzzer) newCodec 39 | ] 40 | , describe "old value with new codec" 41 | [ compatible fuzzer map oldCodec newCodec 42 | ] 43 | ] 44 | 45 | 46 | both : 47 | Fuzzer old 48 | -> (old -> new) 49 | -> Codec old 50 | -> Fuzzer new 51 | -> (new -> old) 52 | -> Codec new 53 | -> List Test 54 | both oldFuzzer oldToNew oldCodec newFuzzer newToOld newCodec = 55 | [ describe "old" 56 | [ Base.roundtrips oldFuzzer oldCodec 57 | ] 58 | , describe "new" 59 | [ Base.roundtrips newFuzzer newCodec 60 | ] 61 | , describe "old value with new codec" 62 | [ compatible oldFuzzer oldToNew oldCodec newCodec 63 | ] 64 | , describe "new value with old codec" 65 | [ compatible newFuzzer newToOld newCodec oldCodec 66 | ] 67 | ] 68 | 69 | 70 | anyToSucceed : List Test 71 | anyToSucceed = 72 | [ forward Fuzz.string (always 3) Codec.string (Codec.succeed 3) 73 | ] 74 | 75 | 76 | type alias Point2 = 77 | { x : Int 78 | , y : Int 79 | } 80 | 81 | 82 | point2Fuzzer : Fuzzer Point2 83 | point2Fuzzer = 84 | Fuzz.map2 Point2 Fuzz.int Fuzz.int 85 | 86 | 87 | point2Codec : Codec Point2 88 | point2Codec = 89 | Codec.object Point2 90 | |> Codec.field "x" .x Codec.int 91 | |> Codec.field "y" .y Codec.int 92 | |> Codec.buildObject 93 | 94 | 95 | type alias Point2_5 = 96 | { x : Int 97 | , y : Int 98 | , z : Maybe Int 99 | } 100 | 101 | 102 | point2_5Fuzzer : Fuzzer Point2_5 103 | point2_5Fuzzer = 104 | Fuzz.map3 Point2_5 Fuzz.int Fuzz.int (Fuzz.maybe Fuzz.int) 105 | 106 | 107 | point2_5Codec : Codec Point2_5 108 | point2_5Codec = 109 | Codec.object Point2_5 110 | |> Codec.field "x" .x Codec.int 111 | |> Codec.field "y" .y Codec.int 112 | |> Codec.maybeField "z" .z Codec.int 113 | |> Codec.buildObject 114 | 115 | 116 | addMaybeField : List Test 117 | addMaybeField = 118 | both 119 | point2Fuzzer 120 | (\{ x, y } -> { x = x, y = y, z = Nothing }) 121 | point2Codec 122 | point2_5Fuzzer 123 | (\{ x, y } -> { x = x, y = y }) 124 | point2_5Codec 125 | -------------------------------------------------------------------------------- /tests/Base.elm: -------------------------------------------------------------------------------- 1 | module Base exposing (roundtrips, suite) 2 | 3 | import Codec exposing (Codec) 4 | import Dict 5 | import Expect 6 | import Fuzz exposing (Fuzzer) 7 | import Set 8 | import Test exposing (Test, describe, fuzz, test) 9 | 10 | 11 | suite : Test 12 | suite = 13 | describe "Testing roundtrips" 14 | [ describe "Basic" basicTests 15 | , describe "Containers" containersTests 16 | , describe "Object" objectTests 17 | , describe "Custom" customTests 18 | , describe "bimap" bimapTests 19 | , describe "maybe" maybeTests 20 | , describe "succeed" 21 | [ test "roundtrips" 22 | (\_ -> 23 | Codec.succeed 632 24 | |> (\d -> Codec.decodeString d "{}") 25 | |> Expect.equal (Ok 632) 26 | ) 27 | ] 28 | , describe "recursive" recursiveTests 29 | , describe "map,andThen" mapAndThenTests 30 | ] 31 | 32 | 33 | roundtrips : Fuzzer a -> Codec a -> Test 34 | roundtrips fuzzer codec = 35 | fuzz fuzzer "is a roundtrip" <| 36 | \value -> 37 | value 38 | |> Codec.encoder codec 39 | |> Codec.decodeValue codec 40 | |> Expect.equal (Ok value) 41 | 42 | 43 | roundtripsWithin : Fuzzer Float -> Codec Float -> Test 44 | roundtripsWithin fuzzer codec = 45 | fuzz fuzzer "is a roundtrip" <| 46 | \value -> 47 | value 48 | |> Codec.encoder codec 49 | |> Codec.decodeValue codec 50 | |> Result.withDefault -999.1234567 51 | |> Expect.within (Expect.Relative 0.000001) value 52 | 53 | 54 | basicTests : List Test 55 | basicTests = 56 | [ describe "Codec.string" 57 | [ roundtrips Fuzz.string Codec.string 58 | ] 59 | , describe "Codec.int" 60 | [ roundtrips Fuzz.int Codec.int 61 | ] 62 | , describe "Codec.float" 63 | [ roundtrips Fuzz.float Codec.float 64 | ] 65 | , describe "Codec.bool" 66 | [ roundtrips Fuzz.bool Codec.bool 67 | ] 68 | ] 69 | 70 | 71 | containersTests : List Test 72 | containersTests = 73 | [ describe "Codec.array" 74 | [ roundtrips (Fuzz.array Fuzz.int) (Codec.array Codec.int) 75 | ] 76 | , describe "Codec.list" 77 | [ roundtrips (Fuzz.list Fuzz.int) (Codec.list Codec.int) 78 | ] 79 | , describe "Codec.dict" 80 | [ roundtrips 81 | (Fuzz.map2 Tuple.pair Fuzz.string Fuzz.int 82 | |> Fuzz.list 83 | |> Fuzz.map Dict.fromList 84 | ) 85 | (Codec.dict Codec.int) 86 | ] 87 | , describe "Codec.set" 88 | [ roundtrips 89 | (Fuzz.list Fuzz.int |> Fuzz.map Set.fromList) 90 | (Codec.set Codec.int) 91 | ] 92 | , describe "Codec.tuple" 93 | [ roundtrips 94 | (Fuzz.tuple ( Fuzz.int, Fuzz.int )) 95 | (Codec.tuple Codec.int Codec.int) 96 | ] 97 | ] 98 | 99 | 100 | objectTests : List Test 101 | objectTests = 102 | [ describe "with 0 fields" 103 | [ roundtrips (Fuzz.constant {}) 104 | (Codec.object {} 105 | |> Codec.buildObject 106 | ) 107 | ] 108 | , describe "with 1 field" 109 | [ roundtrips (Fuzz.map (\i -> { fname = i }) Fuzz.int) 110 | (Codec.object (\i -> { fname = i }) 111 | |> Codec.field "fname" .fname Codec.int 112 | |> Codec.buildObject 113 | ) 114 | ] 115 | , describe "with 2 fields" 116 | [ roundtrips 117 | (Fuzz.map2 118 | (\a b -> 119 | { a = a 120 | , b = b 121 | } 122 | ) 123 | Fuzz.int 124 | Fuzz.int 125 | ) 126 | (Codec.object 127 | (\a b -> 128 | { a = a 129 | , b = b 130 | } 131 | ) 132 | |> Codec.field "a" .a Codec.int 133 | |> Codec.field "b" .b Codec.int 134 | |> Codec.buildObject 135 | ) 136 | ] 137 | , describe "nullableField vs maybeField" <| 138 | let 139 | nullableCodec = 140 | Codec.object 141 | (\f -> { f = f }) 142 | |> Codec.nullableField "f" .f Codec.int 143 | |> Codec.buildObject 144 | 145 | maybeCodec = 146 | Codec.object 147 | (\f -> { f = f }) 148 | |> Codec.maybeField "f" .f Codec.int 149 | |> Codec.buildObject 150 | in 151 | [ test "a nullableField is required" <| 152 | \_ -> 153 | "{}" 154 | |> Codec.decodeString nullableCodec 155 | |> (\r -> 156 | case r of 157 | Ok _ -> 158 | Expect.fail "Should have failed" 159 | 160 | Err _ -> 161 | Expect.pass 162 | ) 163 | , test "a nullableField produces a field with a null value on encoding Nothing" <| 164 | \_ -> 165 | { f = Nothing } 166 | |> Codec.encodeToString 0 nullableCodec 167 | |> Expect.equal "{\"f\":null}" 168 | , test "a maybeField is optional" <| 169 | \_ -> 170 | "{}" 171 | |> Codec.decodeString maybeCodec 172 | |> Expect.equal (Ok { f = Nothing }) 173 | , test "a maybeField doesn't produce a field on encoding Nothing" <| 174 | \_ -> 175 | { f = Nothing } 176 | |> Codec.encodeToString 0 maybeCodec 177 | |> Expect.equal "{}" 178 | ] 179 | ] 180 | 181 | 182 | type Newtype a 183 | = Newtype a 184 | 185 | 186 | customTests : List Test 187 | customTests = 188 | [ describe "with 1 ctor, 0 args" 189 | [ roundtrips (Fuzz.constant ()) 190 | (Codec.custom 191 | (\f v -> 192 | case v of 193 | () -> 194 | f 195 | ) 196 | |> Codec.variant0 "()" () 197 | |> Codec.buildCustom 198 | ) 199 | ] 200 | , describe "with 1 ctor, 1 arg" 201 | [ roundtrips (Fuzz.map Newtype Fuzz.int) 202 | (Codec.custom 203 | (\f v -> 204 | case v of 205 | Newtype a -> 206 | f a 207 | ) 208 | |> Codec.variant1 "Newtype" Newtype Codec.int 209 | |> Codec.buildCustom 210 | ) 211 | ] 212 | , describe "with 2 ctors, 0,1 args" <| 213 | let 214 | match fnothing fjust value = 215 | case value of 216 | Nothing -> 217 | fnothing 218 | 219 | Just v -> 220 | fjust v 221 | 222 | codec = 223 | Codec.custom match 224 | |> Codec.variant0 "Nothing" Nothing 225 | |> Codec.variant1 "Just" Just Codec.int 226 | |> Codec.buildCustom 227 | 228 | fuzzers = 229 | [ ( "1st ctor", Fuzz.constant Nothing ) 230 | , ( "2nd ctor", Fuzz.map Just Fuzz.int ) 231 | ] 232 | in 233 | fuzzers 234 | |> List.map 235 | (\( name, fuzz ) -> 236 | describe name 237 | [ roundtrips fuzz codec ] 238 | ) 239 | ] 240 | 241 | 242 | bimapTests : List Test 243 | bimapTests = 244 | [ roundtripsWithin Fuzz.float <| 245 | Codec.map 246 | (\x -> x * 2) 247 | (\x -> x / 2) 248 | Codec.float 249 | ] 250 | 251 | 252 | maybeTests : List Test 253 | maybeTests = 254 | [ describe "single" 255 | [ roundtrips 256 | (Fuzz.oneOf 257 | [ Fuzz.constant Nothing 258 | , Fuzz.map Just Fuzz.int 259 | ] 260 | ) 261 | <| 262 | Codec.maybe Codec.int 263 | ] 264 | 265 | {- 266 | This is a known limitation: using null as Nothing and identity as Just means that nesting two maybes squashes Just Nothing with Nothing 267 | , describe "double" 268 | [ roundtrips 269 | (Fuzz.oneOf 270 | [ Fuzz.constant Nothing 271 | , Fuzz.constant <| Just Nothing 272 | , Fuzz.map (Just << Just) Fuzz.int 273 | ] 274 | ) 275 | <| 276 | Codec.maybe <| 277 | Codec.maybe Codec.int 278 | ] 279 | -} 280 | ] 281 | 282 | 283 | recursiveTests : List Test 284 | recursiveTests = 285 | [ describe "list" 286 | [ roundtrips (Fuzz.list Fuzz.int) <| 287 | Codec.recursive 288 | (\c -> 289 | Codec.custom 290 | (\fempty fcons value -> 291 | case value of 292 | [] -> 293 | fempty 294 | 295 | x :: xs -> 296 | fcons x xs 297 | ) 298 | |> Codec.variant0 "[]" [] 299 | |> Codec.variant2 "(::)" (::) Codec.int c 300 | |> Codec.buildCustom 301 | ) 302 | ] 303 | ] 304 | 305 | 306 | mapAndThenTests : List Test 307 | mapAndThenTests = 308 | [ describe "Codec.map" 309 | [ roundtrips (Fuzz.intRange -10000 10000) <| 310 | Codec.map (\x -> x - 1) (\x -> x + 1) Codec.int 311 | ] 312 | ] 313 | -------------------------------------------------------------------------------- /src/Codec/Advanced.elm: -------------------------------------------------------------------------------- 1 | module Codec.Advanced exposing (CustomObjectCodec, customObject, objectVariant0, objectVariant1, objectVariant2, objectVariant3, objectVariant4, objectVariant5, objectVariant6, objectVariant7, objectVariant8, buildCustomObject) 2 | 3 | {-| Codecs that can encode/decode objects of a custom shape. These are similar to the codecs for custom types in the `Codec` module, but give you more control over the shape of the result. 4 | 5 | @docs CustomObjectCodec, customObject, objectVariant0, objectVariant1, objectVariant2, objectVariant3, objectVariant4, objectVariant5, objectVariant6, objectVariant7, objectVariant8, buildCustomObject 6 | 7 | -} 8 | 9 | import Codec exposing (Codec, decoder, encoder) 10 | import Dict exposing (Dict) 11 | import Json.Decode as JD exposing (Decoder, Value) 12 | import Json.Encode as JE 13 | 14 | 15 | {-| A partially built `Codec` for an object with a custom shape. 16 | -} 17 | type CustomObjectCodec match v 18 | = CustomCodec 19 | { tagField : String 20 | , match : match 21 | , decoder : Dict String (Decoder v) 22 | } 23 | 24 | 25 | {-| Starts building a `Codec` for an object with a custom shape. 26 | 27 | You need to pass the field name there the variant name will be stored, and a pattern matching function, built like this: 28 | 29 | type Semaphore 30 | = Red Int String 31 | | Yellow 32 | | Green Float 33 | 34 | semaphoreCodec : Codec Semaphore 35 | semaphoreCodec = 36 | Codec.customObject "color" 37 | (\red yellow green value -> 38 | case value of 39 | Red i s -> 40 | red i s 41 | 42 | Yellow -> 43 | yellow 44 | 45 | Green f -> 46 | green f 47 | ) 48 | |> Codec.objectVariant2 "Red" Red ( "width", Codec.int ) ( "label", Codec.string ) 49 | |> Codec.objectVariant0 "Yellow" Yellow 50 | |> Codec.objectVariant1 "Green" Green ( "value", Codec.float ) 51 | |> Codec.buildCustomObject 52 | 53 | This codec would generate and parse values such as `{ "color": "Red", "width": 42, "label": "413" }`, `{ "color": "yellow" }` and `{ "color": "green", "value": 0.42 }` 54 | 55 | -} 56 | customObject : String -> match -> CustomObjectCodec match value 57 | customObject tagField match = 58 | CustomCodec 59 | { tagField = tagField 60 | , match = match 61 | , decoder = Dict.empty 62 | } 63 | 64 | 65 | objectVariant : 66 | String 67 | -> ((List ( String, Value ) -> Value) -> a) 68 | -> Decoder v 69 | -> CustomObjectCodec (a -> b) v 70 | -> CustomObjectCodec b v 71 | objectVariant name matchPiece decoderPiece (CustomCodec am) = 72 | let 73 | enc v = 74 | JE.object <| ( am.tagField, JE.string name ) :: v 75 | in 76 | CustomCodec 77 | { tagField = am.tagField 78 | , match = am.match <| matchPiece enc 79 | , decoder = Dict.insert name decoderPiece am.decoder 80 | } 81 | 82 | 83 | {-| Define a variant with 0 parameters for a custom type. 84 | -} 85 | objectVariant0 : 86 | String 87 | -> v 88 | -> CustomObjectCodec (Value -> c) v 89 | -> CustomObjectCodec c v 90 | objectVariant0 name ctor = 91 | objectVariant name 92 | (\c -> c []) 93 | (JD.succeed ctor) 94 | 95 | 96 | {-| Define a variant with 1 parameter for a custom type. 97 | -} 98 | objectVariant1 : 99 | String 100 | -> (a -> v) 101 | -> ( String, Codec a ) 102 | -> CustomObjectCodec ((a -> Value) -> c) v 103 | -> CustomObjectCodec c v 104 | objectVariant1 name ctor ( f1, m1 ) = 105 | objectVariant name 106 | (\c v1 -> 107 | c 108 | [ ( f1, encoder m1 v1 ) 109 | ] 110 | ) 111 | (JD.map ctor 112 | (JD.field f1 <| decoder m1) 113 | ) 114 | 115 | 116 | {-| Define a variant with 2 parameters for a custom type. 117 | -} 118 | objectVariant2 : 119 | String 120 | -> (a -> b -> v) 121 | -> ( String, Codec a ) 122 | -> ( String, Codec b ) 123 | -> CustomObjectCodec ((a -> b -> Value) -> c) v 124 | -> CustomObjectCodec c v 125 | objectVariant2 name ctor ( f1, m1 ) ( f2, m2 ) = 126 | objectVariant name 127 | (\c v1 v2 -> 128 | c 129 | [ ( f1, encoder m1 v1 ) 130 | , ( f2, encoder m2 v2 ) 131 | ] 132 | ) 133 | (JD.map2 ctor 134 | (JD.field f1 <| decoder m1) 135 | (JD.field f2 <| decoder m2) 136 | ) 137 | 138 | 139 | {-| Define a variant with 4 parameters for a custom type. 140 | -} 141 | objectVariant3 : 142 | String 143 | -> (a -> b -> c -> v) 144 | -> ( String, Codec a ) 145 | -> ( String, Codec b ) 146 | -> ( String, Codec c ) 147 | -> CustomObjectCodec ((a -> b -> c -> Value) -> k) v 148 | -> CustomObjectCodec k v 149 | objectVariant3 name ctor ( f1, m1 ) ( f2, m2 ) ( f3, m3 ) = 150 | objectVariant name 151 | (\c v1 v2 v3 -> 152 | c 153 | [ ( f1, encoder m1 v1 ) 154 | , ( f2, encoder m2 v2 ) 155 | , ( f3, encoder m3 v3 ) 156 | ] 157 | ) 158 | (JD.map3 ctor 159 | (JD.field f1 <| decoder m1) 160 | (JD.field f2 <| decoder m2) 161 | (JD.field f3 <| decoder m3) 162 | ) 163 | 164 | 165 | {-| Define a variant with 4 parameters for a custom type. 166 | -} 167 | objectVariant4 : 168 | String 169 | -> (a -> b -> c -> d -> v) 170 | -> ( String, Codec a ) 171 | -> ( String, Codec b ) 172 | -> ( String, Codec c ) 173 | -> ( String, Codec d ) 174 | -> CustomObjectCodec ((a -> b -> c -> d -> Value) -> k) v 175 | -> CustomObjectCodec k v 176 | objectVariant4 name ctor ( f1, m1 ) ( f2, m2 ) ( f3, m3 ) ( f4, m4 ) = 177 | objectVariant name 178 | (\c v1 v2 v3 v4 -> 179 | c 180 | [ ( f1, encoder m1 v1 ) 181 | , ( f2, encoder m2 v2 ) 182 | , ( f3, encoder m3 v3 ) 183 | , ( f4, encoder m4 v4 ) 184 | ] 185 | ) 186 | (JD.map4 ctor 187 | (JD.field f1 <| decoder m1) 188 | (JD.field f2 <| decoder m2) 189 | (JD.field f3 <| decoder m3) 190 | (JD.field f4 <| decoder m4) 191 | ) 192 | 193 | 194 | {-| Define a variant with 5 parameters for a custom type. 195 | -} 196 | objectVariant5 : 197 | String 198 | -> (a -> b -> c -> d -> e -> v) 199 | -> ( String, Codec a ) 200 | -> ( String, Codec b ) 201 | -> ( String, Codec c ) 202 | -> ( String, Codec d ) 203 | -> ( String, Codec e ) 204 | -> CustomObjectCodec ((a -> b -> c -> d -> e -> Value) -> k) v 205 | -> CustomObjectCodec k v 206 | objectVariant5 name ctor ( f1, m1 ) ( f2, m2 ) ( f3, m3 ) ( f4, m4 ) ( f5, m5 ) = 207 | objectVariant name 208 | (\c v1 v2 v3 v4 v5 -> 209 | c 210 | [ ( f1, encoder m1 v1 ) 211 | , ( f2, encoder m2 v2 ) 212 | , ( f3, encoder m3 v3 ) 213 | , ( f4, encoder m4 v4 ) 214 | , ( f5, encoder m5 v5 ) 215 | ] 216 | ) 217 | (JD.map5 ctor 218 | (JD.field f1 <| decoder m1) 219 | (JD.field f2 <| decoder m2) 220 | (JD.field f3 <| decoder m3) 221 | (JD.field f4 <| decoder m4) 222 | (JD.field f5 <| decoder m5) 223 | ) 224 | 225 | 226 | {-| Define a variant with 6 parameters for a custom type. 227 | -} 228 | objectVariant6 : 229 | String 230 | -> (a -> b -> c -> d -> e -> f -> v) 231 | -> ( String, Codec a ) 232 | -> ( String, Codec b ) 233 | -> ( String, Codec c ) 234 | -> ( String, Codec d ) 235 | -> ( String, Codec e ) 236 | -> ( String, Codec f ) 237 | -> CustomObjectCodec ((a -> b -> c -> d -> e -> f -> Value) -> k) v 238 | -> CustomObjectCodec k v 239 | objectVariant6 name ctor ( f1, m1 ) ( f2, m2 ) ( f3, m3 ) ( f4, m4 ) ( f5, m5 ) ( f6, m6 ) = 240 | objectVariant name 241 | (\c v1 v2 v3 v4 v5 v6 -> 242 | c 243 | [ ( f1, encoder m1 v1 ) 244 | , ( f2, encoder m2 v2 ) 245 | , ( f3, encoder m3 v3 ) 246 | , ( f4, encoder m4 v4 ) 247 | , ( f5, encoder m5 v5 ) 248 | , ( f6, encoder m6 v6 ) 249 | ] 250 | ) 251 | (JD.map6 ctor 252 | (JD.field f1 <| decoder m1) 253 | (JD.field f2 <| decoder m2) 254 | (JD.field f3 <| decoder m3) 255 | (JD.field f4 <| decoder m4) 256 | (JD.field f5 <| decoder m5) 257 | (JD.field f6 <| decoder m6) 258 | ) 259 | 260 | 261 | {-| Define a variant with 7 parameters for a custom type. 262 | -} 263 | objectVariant7 : 264 | String 265 | -> (a -> b -> c -> d -> e -> f -> g -> v) 266 | -> ( String, Codec a ) 267 | -> ( String, Codec b ) 268 | -> ( String, Codec c ) 269 | -> ( String, Codec d ) 270 | -> ( String, Codec e ) 271 | -> ( String, Codec f ) 272 | -> ( String, Codec g ) 273 | -> CustomObjectCodec ((a -> b -> c -> d -> e -> f -> g -> Value) -> k) v 274 | -> CustomObjectCodec k v 275 | objectVariant7 name ctor ( f1, m1 ) ( f2, m2 ) ( f3, m3 ) ( f4, m4 ) ( f5, m5 ) ( f6, m6 ) ( f7, m7 ) = 276 | objectVariant name 277 | (\c v1 v2 v3 v4 v5 v6 v7 -> 278 | c 279 | [ ( f1, encoder m1 v1 ) 280 | , ( f2, encoder m2 v2 ) 281 | , ( f3, encoder m3 v3 ) 282 | , ( f4, encoder m4 v4 ) 283 | , ( f5, encoder m5 v5 ) 284 | , ( f6, encoder m6 v6 ) 285 | , ( f7, encoder m7 v7 ) 286 | ] 287 | ) 288 | (JD.map7 ctor 289 | (JD.field f1 <| decoder m1) 290 | (JD.field f2 <| decoder m2) 291 | (JD.field f3 <| decoder m3) 292 | (JD.field f4 <| decoder m4) 293 | (JD.field f5 <| decoder m5) 294 | (JD.field f6 <| decoder m6) 295 | (JD.field f7 <| decoder m7) 296 | ) 297 | 298 | 299 | {-| Define a variant with 8 parameters for a custom type. 300 | -} 301 | objectVariant8 : 302 | String 303 | -> (a -> b -> c -> d -> e -> f -> g -> h -> v) 304 | -> ( String, Codec a ) 305 | -> ( String, Codec b ) 306 | -> ( String, Codec c ) 307 | -> ( String, Codec d ) 308 | -> ( String, Codec e ) 309 | -> ( String, Codec f ) 310 | -> ( String, Codec g ) 311 | -> ( String, Codec h ) 312 | -> CustomObjectCodec ((a -> b -> c -> d -> e -> f -> g -> h -> Value) -> k) v 313 | -> CustomObjectCodec k v 314 | objectVariant8 name ctor ( f1, m1 ) ( f2, m2 ) ( f3, m3 ) ( f4, m4 ) ( f5, m5 ) ( f6, m6 ) ( f7, m7 ) ( f8, m8 ) = 315 | objectVariant name 316 | (\c v1 v2 v3 v4 v5 v6 v7 v8 -> 317 | c 318 | [ ( f1, encoder m1 v1 ) 319 | , ( f2, encoder m2 v2 ) 320 | , ( f3, encoder m3 v3 ) 321 | , ( f4, encoder m4 v4 ) 322 | , ( f5, encoder m5 v5 ) 323 | , ( f6, encoder m6 v6 ) 324 | , ( f7, encoder m7 v7 ) 325 | , ( f8, encoder m8 v8 ) 326 | ] 327 | ) 328 | (JD.map8 ctor 329 | (JD.field f1 <| decoder m1) 330 | (JD.field f2 <| decoder m2) 331 | (JD.field f3 <| decoder m3) 332 | (JD.field f4 <| decoder m4) 333 | (JD.field f5 <| decoder m5) 334 | (JD.field f6 <| decoder m6) 335 | (JD.field f7 <| decoder m7) 336 | (JD.field f8 <| decoder m8) 337 | ) 338 | 339 | 340 | {-| Build a `Codec` for a fully specified custom type. 341 | -} 342 | buildCustomObject : CustomObjectCodec (a -> Value) a -> Codec a 343 | buildCustomObject (CustomCodec am) = 344 | Codec.build am.match 345 | (JD.field am.tagField JD.string 346 | |> JD.andThen 347 | (\tag -> 348 | Dict.get tag am.decoder 349 | |> Maybe.withDefault 350 | (JD.fail <| am.tagField ++ " \"" ++ tag ++ "\" did not match") 351 | ) 352 | ) 353 | -------------------------------------------------------------------------------- /src/Codec.elm: -------------------------------------------------------------------------------- 1 | module Codec exposing 2 | ( Codec, Value, Error 3 | , Decoder, decoder, decodeString, decodeValue 4 | , encoder, encodeToString, encodeToValue 5 | , string, bool, int, float, char 6 | , maybe, list, array, dict, set, tuple, triple, result 7 | , ObjectCodec, object, field, maybeField, nullableField, buildObject 8 | , CustomCodec, custom, variant0, variant1, variant2, variant3, variant4, variant5, variant6, variant7, variant8, buildCustom 9 | , oneOf 10 | , map 11 | , succeed, recursive, fail, andThen, lazy, value, build, constant 12 | ) 13 | 14 | {-| A `Codec a` contain a JSON `Decoder a` and the corresponding `a -> Value` encoder. 15 | 16 | 17 | # Definition 18 | 19 | @docs Codec, Value, Error 20 | 21 | 22 | # Decode 23 | 24 | @docs Decoder, decoder, decodeString, decodeValue 25 | 26 | 27 | # Encode 28 | 29 | @docs encoder, encodeToString, encodeToValue 30 | 31 | 32 | # Primitives 33 | 34 | @docs string, bool, int, float, char 35 | 36 | 37 | # Data Structures 38 | 39 | @docs maybe, list, array, dict, set, tuple, triple, result 40 | 41 | 42 | # Object Primitives 43 | 44 | @docs ObjectCodec, object, field, maybeField, nullableField, buildObject 45 | 46 | 47 | # Custom Types 48 | 49 | @docs CustomCodec, custom, variant0, variant1, variant2, variant3, variant4, variant5, variant6, variant7, variant8, buildCustom 50 | 51 | 52 | # Inconsistent structure 53 | 54 | @docs oneOf 55 | 56 | 57 | # Mapping 58 | 59 | @docs map 60 | 61 | 62 | # Fancy Codecs 63 | 64 | @docs succeed, recursive, fail, andThen, lazy, value, build, constant 65 | 66 | -} 67 | 68 | import Array exposing (Array) 69 | import Dict exposing (Dict) 70 | import Json.Decode as JD 71 | import Json.Encode as JE 72 | import Set exposing (Set) 73 | 74 | 75 | 76 | -- DEFINITION 77 | 78 | 79 | {-| A value that knows how to encode and decode JSON values. 80 | -} 81 | type Codec a 82 | = Codec 83 | { encoder : a -> Value 84 | , decoder : Decoder a 85 | } 86 | 87 | 88 | {-| Represents a JavaScript value. 89 | -} 90 | type alias Value = 91 | JE.Value 92 | 93 | 94 | {-| A structured error describing exactly how the decoder failed. You can use 95 | this to create more elaborate visualizations of a decoder problem. For example, 96 | you could show the entire JSON object and show the part causing the failure in 97 | red. 98 | -} 99 | type alias Error = 100 | JD.Error 101 | 102 | 103 | 104 | -- DECODE 105 | 106 | 107 | {-| A value that knows how to decode JSON values. 108 | -} 109 | type alias Decoder a = 110 | JD.Decoder a 111 | 112 | 113 | {-| Extracts the `Decoder` contained inside the `Codec`. 114 | -} 115 | decoder : Codec a -> Decoder a 116 | decoder (Codec m) = 117 | m.decoder 118 | 119 | 120 | {-| Parse the given string into a JSON value and then run the `Codec` on it. 121 | This will fail if the string is not well-formed JSON or if the `Codec` 122 | fails for some reason. 123 | -} 124 | decodeString : Codec a -> String -> Result Error a 125 | decodeString codec = 126 | JD.decodeString (decoder codec) 127 | 128 | 129 | {-| Run a `Codec` to decode some JSON `Value`. You can send these JSON values 130 | through ports, so that is probably the main time you would use this function. 131 | -} 132 | decodeValue : Codec a -> Value -> Result Error a 133 | decodeValue codec = 134 | JD.decodeValue (decoder codec) 135 | 136 | 137 | 138 | -- ENCODE 139 | 140 | 141 | {-| Extracts the encoding function contained inside the `Codec`. 142 | -} 143 | encoder : Codec a -> a -> Value 144 | encoder (Codec m) = 145 | m.encoder 146 | 147 | 148 | {-| Convert a value into a prettified JSON string. The first argument specifies 149 | the amount of indentation in the result string. 150 | -} 151 | encodeToString : Int -> Codec a -> a -> String 152 | encodeToString indentation codec = 153 | encoder codec >> JE.encode indentation 154 | 155 | 156 | {-| Convert a value into a Javascript `Value`. 157 | -} 158 | encodeToValue : Codec a -> a -> Value 159 | encodeToValue codec = 160 | encoder codec 161 | 162 | 163 | 164 | -- BASE 165 | 166 | 167 | {-| Build your own custom `Codec`. 168 | Useful if you have pre-existing `Decoder`s you need to use. 169 | -} 170 | build : (a -> Value) -> Decoder a -> Codec a 171 | build encoder_ decoder_ = 172 | Codec 173 | { encoder = encoder_ 174 | , decoder = decoder_ 175 | } 176 | 177 | 178 | {-| `Codec` between a JSON string and an Elm `String` 179 | -} 180 | string : Codec String 181 | string = 182 | build JE.string JD.string 183 | 184 | 185 | {-| `Codec` between a JSON boolean and an Elm `Bool` 186 | -} 187 | bool : Codec Bool 188 | bool = 189 | build JE.bool JD.bool 190 | 191 | 192 | {-| `Codec` between a JSON number and an Elm `Int` 193 | -} 194 | int : Codec Int 195 | int = 196 | build JE.int JD.int 197 | 198 | 199 | {-| `Codec` between a JSON number and an Elm `Float` 200 | -} 201 | float : Codec Float 202 | float = 203 | build JE.float JD.float 204 | 205 | 206 | {-| `Codec` between a JSON string of length 1 and an Elm `Char` 207 | -} 208 | char : Codec Char 209 | char = 210 | build 211 | (String.fromChar >> JE.string) 212 | (JD.string 213 | |> JD.andThen 214 | (\s -> 215 | case String.uncons s of 216 | Just ( h, "" ) -> 217 | JD.succeed h 218 | 219 | _ -> 220 | JD.fail "Expected a single char" 221 | ) 222 | ) 223 | 224 | 225 | 226 | -- DATA STRUCTURES 227 | 228 | 229 | composite : ((b -> Value) -> (a -> Value)) -> (Decoder b -> Decoder a) -> Codec b -> Codec a 230 | composite enc dec (Codec codec) = 231 | Codec 232 | { encoder = enc codec.encoder 233 | , decoder = dec codec.decoder 234 | } 235 | 236 | 237 | {-| Represents an optional value. 238 | -} 239 | maybe : Codec a -> Codec (Maybe a) 240 | maybe codec = 241 | Codec 242 | { decoder = JD.maybe <| decoder codec 243 | , encoder = 244 | \v -> 245 | case v of 246 | Nothing -> 247 | JE.null 248 | 249 | Just x -> 250 | encoder codec x 251 | } 252 | 253 | 254 | {-| `Codec` between a JSON array and an Elm `List`. 255 | -} 256 | list : Codec a -> Codec (List a) 257 | list = 258 | composite JE.list JD.list 259 | 260 | 261 | {-| `Codec` between a JSON array and an Elm `Array`. 262 | -} 263 | array : Codec a -> Codec (Array a) 264 | array = 265 | composite JE.array JD.array 266 | 267 | 268 | {-| `Codec` between a JSON object and an Elm `Dict`. 269 | -} 270 | dict : Codec a -> Codec (Dict String a) 271 | dict = 272 | composite 273 | (\e -> JE.object << Dict.toList << Dict.map (\_ -> e)) 274 | JD.dict 275 | 276 | 277 | {-| `Codec` between a JSON array and an Elm `Set`. 278 | -} 279 | set : Codec comparable -> Codec (Set comparable) 280 | set = 281 | composite 282 | (\e -> JE.list e << Set.toList) 283 | (JD.map Set.fromList << JD.list) 284 | 285 | 286 | {-| `Codec` between a JSON array of length 2 and an Elm `Tuple`. 287 | -} 288 | tuple : Codec a -> Codec b -> Codec ( a, b ) 289 | tuple m1 m2 = 290 | Codec 291 | { encoder = 292 | \( v1, v2 ) -> 293 | JE.list identity 294 | [ encoder m1 v1 295 | , encoder m2 v2 296 | ] 297 | , decoder = 298 | JD.map2 299 | (\a b -> ( a, b )) 300 | (JD.index 0 <| decoder m1) 301 | (JD.index 1 <| decoder m2) 302 | } 303 | 304 | 305 | {-| `Codec` between a JSON array of length 3 and an Elm triple. 306 | -} 307 | triple : Codec a -> Codec b -> Codec c -> Codec ( a, b, c ) 308 | triple m1 m2 m3 = 309 | Codec 310 | { encoder = 311 | \( v1, v2, v3 ) -> 312 | JE.list identity 313 | [ encoder m1 v1 314 | , encoder m2 v2 315 | , encoder m3 v3 316 | ] 317 | , decoder = 318 | JD.map3 319 | (\a b c -> ( a, b, c )) 320 | (JD.index 0 <| decoder m1) 321 | (JD.index 1 <| decoder m2) 322 | (JD.index 2 <| decoder m3) 323 | } 324 | 325 | 326 | {-| `Codec` for `Result` values. 327 | -} 328 | result : Codec error -> Codec value -> Codec (Result error value) 329 | result errorCodec valueCodec = 330 | custom 331 | (\ferr fok v -> 332 | case v of 333 | Err err -> 334 | ferr err 335 | 336 | Ok ok -> 337 | fok ok 338 | ) 339 | |> variant1 "Err" Err errorCodec 340 | |> variant1 "Ok" Ok valueCodec 341 | |> buildCustom 342 | 343 | 344 | 345 | -- OBJECTS 346 | 347 | 348 | {-| A partially built `Codec` for an object. 349 | -} 350 | type ObjectCodec a b 351 | = ObjectCodec 352 | { encoder : a -> List ( String, Value ) 353 | , decoder : Decoder b 354 | } 355 | 356 | 357 | {-| Start creating a `Codec` for an object. You should pass the main constructor as argument. 358 | If you don't have one (for example it's a simple type with no name), you should pass a function that given the field values builds an object. 359 | 360 | Example with constructor: 361 | 362 | type alias Point = 363 | { x : Float 364 | , y : Float 365 | } 366 | 367 | pointCodec : Codec Point 368 | pointCodec = 369 | Codec.object Point 370 | |> Codec.field "x" .x Codec.float 371 | |> Codec.field "y" .y Codec.float 372 | |> Codec.buildObject 373 | 374 | Example without constructor: 375 | 376 | pointCodec : Codec { x : Int, y : Bool } 377 | pointCodec = 378 | Codec.object (\x y -> { x = x, y = y }) 379 | |> Codec.field "x" .x Codec.int 380 | |> Codec.field "y" .y Codec.bool 381 | |> Codec.buildObject 382 | 383 | -} 384 | object : b -> ObjectCodec a b 385 | object ctor = 386 | ObjectCodec 387 | { encoder = \_ -> [] 388 | , decoder = JD.succeed ctor 389 | } 390 | 391 | 392 | {-| Specify the name, getter and `Codec` for a field. 393 | 394 | The name is only used as the field name in the resulting JSON, and has no impact on the Elm side. 395 | 396 | -} 397 | field : String -> (a -> f) -> Codec f -> ObjectCodec a (f -> b) -> ObjectCodec a b 398 | field name getter codec (ObjectCodec ocodec) = 399 | ObjectCodec 400 | { encoder = \v -> ( name, encoder codec <| getter v ) :: ocodec.encoder v 401 | , decoder = JD.map2 (\f x -> f x) ocodec.decoder (JD.field name (decoder codec)) 402 | } 403 | 404 | 405 | {-| Specify the name getter and `Codec` for an optional field. 406 | 407 | This is particularly useful for evolving your `Codec`s. 408 | 409 | If the field is not present in the input then it gets decoded to `Nothing`. 410 | If the optional field's value is `Nothing` then the resulting object will not contain that field. 411 | 412 | -} 413 | maybeField : String -> (a -> Maybe f) -> Codec f -> ObjectCodec a (Maybe f -> b) -> ObjectCodec a b 414 | maybeField name getter codec (ObjectCodec ocodec) = 415 | ObjectCodec 416 | { encoder = 417 | \v -> 418 | case getter v of 419 | Just present -> 420 | ( name, encoder codec present ) :: ocodec.encoder v 421 | 422 | Nothing -> 423 | ocodec.encoder v 424 | , decoder = 425 | decoder codec 426 | |> JD.field name 427 | |> JD.maybe 428 | |> JD.map2 (\f x -> f x) ocodec.decoder 429 | } 430 | 431 | 432 | {-| Specify the name getter and `Codec` for a required field, whose value can be `null`. 433 | 434 | If the field is not present in the input then _the decoding fails_. 435 | If the field's value is `Nothing` then the resulting object will contain the field with a `null` value. 436 | 437 | This is a shorthand for a field having a codec built using `Codec.maybe`. 438 | 439 | -} 440 | nullableField : String -> (a -> Maybe f) -> Codec f -> ObjectCodec a (Maybe f -> b) -> ObjectCodec a b 441 | nullableField name getter codec ocodec = 442 | field name getter (maybe codec) ocodec 443 | 444 | 445 | {-| Create a `Codec` from a fully specified `ObjectCodec`. 446 | -} 447 | buildObject : ObjectCodec a a -> Codec a 448 | buildObject (ObjectCodec om) = 449 | Codec 450 | { encoder = \v -> JE.object <| List.reverse <| om.encoder v 451 | , decoder = om.decoder 452 | } 453 | 454 | 455 | 456 | -- CUSTOM 457 | 458 | 459 | {-| A partially built `Codec` for a custom type. 460 | -} 461 | type CustomCodec match v 462 | = CustomCodec 463 | { match : match 464 | , decoder : Dict String (Decoder v) 465 | } 466 | 467 | 468 | {-| Starts building a `Codec` for a custom type. 469 | 470 | You need to pass a pattern matching function, built like this: 471 | 472 | type Semaphore 473 | = Red Int String 474 | | Yellow 475 | | Green Float 476 | 477 | semaphoreCodec : Codec Semaphore 478 | semaphoreCodec = 479 | Codec.custom 480 | (\red yellow green value -> 481 | case value of 482 | Red i s -> 483 | red i s 484 | 485 | Yellow -> 486 | yellow 487 | 488 | Green f -> 489 | green f 490 | ) 491 | |> Codec.variant2 "Red" Red Codec.int Codec.string 492 | |> Codec.variant0 "Yellow" Yellow 493 | |> Codec.variant1 "Green" Green Codec.float 494 | |> Codec.buildCustom 495 | 496 | -} 497 | custom : match -> CustomCodec match value 498 | custom match = 499 | CustomCodec 500 | { match = match 501 | , decoder = Dict.empty 502 | } 503 | 504 | 505 | variant : 506 | String 507 | -> ((List Value -> Value) -> a) 508 | -> Decoder v 509 | -> CustomCodec (a -> b) v 510 | -> CustomCodec b v 511 | variant name matchPiece decoderPiece (CustomCodec am) = 512 | let 513 | enc v = 514 | JE.object 515 | [ ( "tag", JE.string name ) 516 | , ( "args", JE.list identity v ) 517 | ] 518 | in 519 | CustomCodec 520 | { match = am.match <| matchPiece enc 521 | , decoder = Dict.insert name decoderPiece am.decoder 522 | } 523 | 524 | 525 | {-| Define a variant with 0 parameters for a custom type. 526 | -} 527 | variant0 : 528 | String 529 | -> v 530 | -> CustomCodec (Value -> a) v 531 | -> CustomCodec a v 532 | variant0 name ctor = 533 | variant name 534 | (\c -> c []) 535 | (JD.succeed ctor) 536 | 537 | 538 | {-| Define a variant with 1 parameters for a custom type. 539 | -} 540 | variant1 : 541 | String 542 | -> (a -> v) 543 | -> Codec a 544 | -> CustomCodec ((a -> Value) -> b) v 545 | -> CustomCodec b v 546 | variant1 name ctor m1 = 547 | variant name 548 | (\c v -> 549 | c 550 | [ encoder m1 v 551 | ] 552 | ) 553 | (JD.map ctor 554 | (JD.index 0 <| decoder m1) 555 | ) 556 | 557 | 558 | {-| Define a variant with 2 parameters for a custom type. 559 | -} 560 | variant2 : 561 | String 562 | -> (a -> b -> v) 563 | -> Codec a 564 | -> Codec b 565 | -> CustomCodec ((a -> b -> Value) -> c) v 566 | -> CustomCodec c v 567 | variant2 name ctor m1 m2 = 568 | variant name 569 | (\c v1 v2 -> 570 | c 571 | [ encoder m1 v1 572 | , encoder m2 v2 573 | ] 574 | ) 575 | (JD.map2 ctor 576 | (JD.index 0 <| decoder m1) 577 | (JD.index 1 <| decoder m2) 578 | ) 579 | 580 | 581 | {-| Define a variant with 3 parameters for a custom type. 582 | -} 583 | variant3 : 584 | String 585 | -> (a -> b -> c -> v) 586 | -> Codec a 587 | -> Codec b 588 | -> Codec c 589 | -> CustomCodec ((a -> b -> c -> Value) -> partial) v 590 | -> CustomCodec partial v 591 | variant3 name ctor m1 m2 m3 = 592 | variant name 593 | (\c v1 v2 v3 -> 594 | c 595 | [ encoder m1 v1 596 | , encoder m2 v2 597 | , encoder m3 v3 598 | ] 599 | ) 600 | (JD.map3 ctor 601 | (JD.index 0 <| decoder m1) 602 | (JD.index 1 <| decoder m2) 603 | (JD.index 2 <| decoder m3) 604 | ) 605 | 606 | 607 | {-| Define a variant with 4 parameters for a custom type. 608 | -} 609 | variant4 : 610 | String 611 | -> (a -> b -> c -> d -> v) 612 | -> Codec a 613 | -> Codec b 614 | -> Codec c 615 | -> Codec d 616 | -> CustomCodec ((a -> b -> c -> d -> Value) -> partial) v 617 | -> CustomCodec partial v 618 | variant4 name ctor m1 m2 m3 m4 = 619 | variant name 620 | (\c v1 v2 v3 v4 -> 621 | c 622 | [ encoder m1 v1 623 | , encoder m2 v2 624 | , encoder m3 v3 625 | , encoder m4 v4 626 | ] 627 | ) 628 | (JD.map4 ctor 629 | (JD.index 0 <| decoder m1) 630 | (JD.index 1 <| decoder m2) 631 | (JD.index 2 <| decoder m3) 632 | (JD.index 3 <| decoder m4) 633 | ) 634 | 635 | 636 | {-| Define a variant with 5 parameters for a custom type. 637 | -} 638 | variant5 : 639 | String 640 | -> (a -> b -> c -> d -> e -> v) 641 | -> Codec a 642 | -> Codec b 643 | -> Codec c 644 | -> Codec d 645 | -> Codec e 646 | -> CustomCodec ((a -> b -> c -> d -> e -> Value) -> partial) v 647 | -> CustomCodec partial v 648 | variant5 name ctor m1 m2 m3 m4 m5 = 649 | variant name 650 | (\c v1 v2 v3 v4 v5 -> 651 | c 652 | [ encoder m1 v1 653 | , encoder m2 v2 654 | , encoder m3 v3 655 | , encoder m4 v4 656 | , encoder m5 v5 657 | ] 658 | ) 659 | (JD.map5 ctor 660 | (JD.index 0 <| decoder m1) 661 | (JD.index 1 <| decoder m2) 662 | (JD.index 2 <| decoder m3) 663 | (JD.index 3 <| decoder m4) 664 | (JD.index 4 <| decoder m5) 665 | ) 666 | 667 | 668 | {-| Define a variant with 6 parameters for a custom type. 669 | -} 670 | variant6 : 671 | String 672 | -> (a -> b -> c -> d -> e -> f -> v) 673 | -> Codec a 674 | -> Codec b 675 | -> Codec c 676 | -> Codec d 677 | -> Codec e 678 | -> Codec f 679 | -> CustomCodec ((a -> b -> c -> d -> e -> f -> Value) -> partial) v 680 | -> CustomCodec partial v 681 | variant6 name ctor m1 m2 m3 m4 m5 m6 = 682 | variant name 683 | (\c v1 v2 v3 v4 v5 v6 -> 684 | c 685 | [ encoder m1 v1 686 | , encoder m2 v2 687 | , encoder m3 v3 688 | , encoder m4 v4 689 | , encoder m5 v5 690 | , encoder m6 v6 691 | ] 692 | ) 693 | (JD.map6 ctor 694 | (JD.index 0 <| decoder m1) 695 | (JD.index 1 <| decoder m2) 696 | (JD.index 2 <| decoder m3) 697 | (JD.index 3 <| decoder m4) 698 | (JD.index 4 <| decoder m5) 699 | (JD.index 5 <| decoder m6) 700 | ) 701 | 702 | 703 | {-| Define a variant with 7 parameters for a custom type. 704 | -} 705 | variant7 : 706 | String 707 | -> (a -> b -> c -> d -> e -> f -> g -> v) 708 | -> Codec a 709 | -> Codec b 710 | -> Codec c 711 | -> Codec d 712 | -> Codec e 713 | -> Codec f 714 | -> Codec g 715 | -> CustomCodec ((a -> b -> c -> d -> e -> f -> g -> Value) -> partial) v 716 | -> CustomCodec partial v 717 | variant7 name ctor m1 m2 m3 m4 m5 m6 m7 = 718 | variant name 719 | (\c v1 v2 v3 v4 v5 v6 v7 -> 720 | c 721 | [ encoder m1 v1 722 | , encoder m2 v2 723 | , encoder m3 v3 724 | , encoder m4 v4 725 | , encoder m5 v5 726 | , encoder m6 v6 727 | , encoder m7 v7 728 | ] 729 | ) 730 | (JD.map7 ctor 731 | (JD.index 0 <| decoder m1) 732 | (JD.index 1 <| decoder m2) 733 | (JD.index 2 <| decoder m3) 734 | (JD.index 3 <| decoder m4) 735 | (JD.index 4 <| decoder m5) 736 | (JD.index 5 <| decoder m6) 737 | (JD.index 6 <| decoder m7) 738 | ) 739 | 740 | 741 | {-| Define a variant with 8 parameters for a custom type. 742 | -} 743 | variant8 : 744 | String 745 | -> (a -> b -> c -> d -> e -> f -> g -> h -> v) 746 | -> Codec a 747 | -> Codec b 748 | -> Codec c 749 | -> Codec d 750 | -> Codec e 751 | -> Codec f 752 | -> Codec g 753 | -> Codec h 754 | -> CustomCodec ((a -> b -> c -> d -> e -> f -> g -> h -> Value) -> partial) v 755 | -> CustomCodec partial v 756 | variant8 name ctor m1 m2 m3 m4 m5 m6 m7 m8 = 757 | variant name 758 | (\c v1 v2 v3 v4 v5 v6 v7 v8 -> 759 | c 760 | [ encoder m1 v1 761 | , encoder m2 v2 762 | , encoder m3 v3 763 | , encoder m4 v4 764 | , encoder m5 v5 765 | , encoder m6 v6 766 | , encoder m7 v7 767 | , encoder m8 v8 768 | ] 769 | ) 770 | (JD.map8 ctor 771 | (JD.index 0 <| decoder m1) 772 | (JD.index 1 <| decoder m2) 773 | (JD.index 2 <| decoder m3) 774 | (JD.index 3 <| decoder m4) 775 | (JD.index 4 <| decoder m5) 776 | (JD.index 5 <| decoder m6) 777 | (JD.index 6 <| decoder m7) 778 | (JD.index 7 <| decoder m8) 779 | ) 780 | 781 | 782 | {-| Build a `Codec` for a fully specified custom type. 783 | -} 784 | buildCustom : CustomCodec (a -> Value) a -> Codec a 785 | buildCustom (CustomCodec am) = 786 | Codec 787 | { encoder = \v -> am.match v 788 | , decoder = 789 | JD.field "tag" JD.string 790 | |> JD.andThen 791 | (\tag -> 792 | case Dict.get tag am.decoder of 793 | Nothing -> 794 | JD.fail <| "tag " ++ tag ++ " did not match" 795 | 796 | Just dec -> 797 | JD.field "args" dec 798 | ) 799 | } 800 | 801 | 802 | 803 | -- INCONSISTENT STRUCTURE 804 | 805 | 806 | {-| Try a set of decoders (in order). 807 | The first argument is used for encoding and decoding, the list of other codecs is used as a fallback while decoding. 808 | 809 | This is particularly useful for backwards compatibility. You would pass the current codec as the first argument, 810 | and the old ones (eventually `map`ped) as a fallback list to use while decoding. 811 | 812 | -} 813 | oneOf : Codec a -> List (Codec a) -> Codec a 814 | oneOf main alts = 815 | Codec 816 | { encoder = encoder main 817 | , decoder = JD.oneOf <| decoder main :: List.map decoder alts 818 | } 819 | 820 | 821 | 822 | -- MAPPING 823 | 824 | 825 | {-| Transform a `Codec`. 826 | -} 827 | map : (a -> b) -> (b -> a) -> Codec a -> Codec b 828 | map go back codec = 829 | Codec 830 | { decoder = JD.map go <| decoder codec 831 | , encoder = \v -> back v |> encoder codec 832 | } 833 | 834 | 835 | 836 | -- FANCY 837 | 838 | 839 | {-| Ignore the JSON and make the decoder fail. This is handy when used with 840 | `oneOf` or `andThen` where you want to give a custom error message in some 841 | case. The encoder will produce `null`. 842 | -} 843 | fail : String -> Codec a 844 | fail msg = 845 | Codec 846 | { decoder = JD.fail msg 847 | , encoder = always JE.null 848 | } 849 | 850 | 851 | {-| Create codecs that depend on previous results. 852 | -} 853 | andThen : (a -> Codec b) -> (b -> a) -> Codec a -> Codec b 854 | andThen dec enc c = 855 | Codec 856 | { decoder = decoder c |> JD.andThen (dec >> decoder) 857 | , encoder = encoder c << enc 858 | } 859 | 860 | 861 | {-| Create a `Codec` for a recursive data structure. 862 | The argument to the function you need to pass is the fully formed `Codec`. 863 | -} 864 | recursive : (Codec a -> Codec a) -> Codec a 865 | recursive f = 866 | f <| lazy (\_ -> recursive f) 867 | 868 | 869 | {-| Create a `Codec` that produces null as JSON and always decodes as the same value. 870 | -} 871 | succeed : a -> Codec a 872 | succeed default_ = 873 | Codec 874 | { decoder = JD.succeed default_ 875 | , encoder = \_ -> JE.null 876 | } 877 | 878 | 879 | {-| Create a `Codec` that produces null as JSON and always decodes as the same value. Obsolete alias of `succeed`, will be removed in a future version. 880 | -} 881 | constant : a -> Codec a 882 | constant = 883 | succeed 884 | 885 | 886 | {-| This is useful for recursive structures that are not easily modeled with `recursive`. 887 | Have a look at the Json.Decode docs for examples. 888 | -} 889 | lazy : (() -> Codec a) -> Codec a 890 | lazy f = 891 | Codec 892 | { decoder = JD.lazy (\_ -> decoder <| f ()) 893 | , encoder = \v -> encoder (f ()) v 894 | } 895 | 896 | 897 | {-| Create a `Codec` that doesn't transform the JSON value, just brings it to and from Elm as a `Value`. 898 | -} 899 | value : Codec Value 900 | value = 901 | Codec 902 | { encoder = identity 903 | , decoder = JD.value 904 | } 905 | --------------------------------------------------------------------------------