module Elm.Review.Vendor.Serialize exposing ( encodeToJson, decodeFromJson , Codec, Error(..) , string, bool, float, int, unit , maybe, list, array, dict, set, tuple, triple, result, enum , RecordCodec, record, field, finishRecord , CustomTypeCodec, customType, variant0, variant1, variant2, variant3, variant4, variant5, variant6, variant7, variant8, finishCustomType, VariantEncoder , map, mapValid, mapError , lazy ) {-| # Serialization You have three options when encoding data. You can represent the data either as json, bytes, or a string. Here's some advice when choosing: - If performance is important, use `encodeToJson` and `decodeFromJson` - If space efficiency is important, use `encodeToBytes` and `decodeFromBytes`\* - `encodeToString` and `decodeFromString` are good for URL safe strings but otherwise one of the other choices is probably better. \*`encodeToJson` is more compact when encoding integers with 6 or fewer digits. You may want to try both `encodeToBytes` and `encodeToJson` and see which is better for your use case. @docs encodeToJson, decodeFromJson, encodeToBytes, decodeFromBytes, encodeToString, decodeFromString # Definition @docs Codec, Error # Primitives @docs string, bool, float, int, unit, bytes, byte # Data Structures @docs maybe, list, array, dict, set, tuple, triple, result, enum # Records @docs RecordCodec, record, field, finishRecord # Custom Types @docs CustomTypeCodec, customType, variant0, variant1, variant2, variant3, variant4, variant5, variant6, variant7, variant8, finishCustomType, VariantEncoder # Mapping @docs map, mapValid, mapError # Stack unsafe @docs lazy -} import Array exposing (Array) import Dict exposing (Dict) import Json.Decode as JD import Json.Encode as JE import Set exposing (Set) type T4 a b c d = T4 a b c d type T5 a b c d e = T5 a b c d e type T6 a b c d e f = T6 a b c d e f type T7 a b c d e f g = T7 a b c d e f g type T8 a b c d e f g h = T8 a b c d e f g h -- DEFINITION {-| A value that knows how to encode and decode an Elm data structure. -} type Codec e a = Codec { jsonEncoder : a -> JE.Value , jsonDecoder : JD.Decoder (Result (Error e) a) } {-| Possible errors that can occur when decoding. - `CustomError` - An error caused by `andThen` returning an Err value. - `DataCorrupted` - This most likely will occur if you make breaking changes to your codec and try to decode old data\*. Have a look at `How do I change my codecs and still be able to decode old data?` in the readme for how to avoid introducing breaking changes. - `SerializerOutOfDate` - When encoding, this package will include a version number. This makes it possible for me to make improvements to how data gets encoded without introducing breaking changes to your codecs. This error then, says that you're trying to decode data encoded with a newer version of elm-serialize. \*It's possible for corrupted data to still succeed in decoding (but with nonsense Elm values). This is because internally we're just encoding Elm values and not storing any kind of structural information. So if you encoded an Int and then a Float, and then tried decoding it as a Float and then an Int, there's no way for the decoder to know it read the data in the wrong order. -} type Error e = CustomError e | DataCorrupted | SerializerOutOfDate version : Int version = 1 {-| Extracts the json `Decoder` contained inside the `Codec`. -} getJsonDecoder : Codec e a -> JD.Decoder (Result (Error e) a) getJsonDecoder (Codec m) = m.jsonDecoder {-| Run a `Codec` to turn a json value encoded with `encodeToJson` into an Elm value. -} decodeFromJson : Codec e a -> JE.Value -> Result (Error e) a decodeFromJson codec json = let decoder = JD.index 0 JD.int |> JD.andThen (\value -> if value <= 0 then Err DataCorrupted |> JD.succeed else if value == version then JD.index 1 (getJsonDecoder codec) else Err SerializerOutOfDate |> JD.succeed ) in case JD.decodeValue decoder json of Ok value -> value Err _ -> Err DataCorrupted -- ENCODE {-| Extracts the json encoding function contained inside the `Codec`. -} getJsonEncoder : Codec e a -> a -> JE.Value getJsonEncoder (Codec m) = m.jsonEncoder {-| Convert an Elm value into json data. -} encodeToJson : Codec e a -> a -> JE.Value encodeToJson codec value = JE.list identity [ JE.int version , value |> getJsonEncoder codec ] -- BASE build : (a -> JE.Value) -> JD.Decoder (Result (Error e) a) -> Codec e a build jsonEncoder jsonDecoder = Codec { jsonEncoder = jsonEncoder , jsonDecoder = jsonDecoder } {-| Codec for serializing a `String` -} string : Codec e String string = build JE.string (JD.string |> JD.map Ok) {-| Codec for serializing a `Bool` -} bool : Codec e Bool bool = build JE.bool (JD.bool |> JD.map Ok) {-| Codec for serializing an `Int` -} int : Codec e Int int = build JE.int (JD.int |> JD.map Ok) {-| Codec for serializing a `Float` -} float : Codec e Float float = build JE.float (JD.float |> JD.map Ok) -- DATA STRUCTURES {-| Codec for serializing a `Maybe` import Serialize as S maybeIntCodec : S.Codec (Maybe Int) maybeIntCodec = S.Maybe S.Int -} maybe : Codec e a -> Codec e (Maybe a) maybe justCodec = customType (\nothingEncoder justEncoder value -> case value of Nothing -> nothingEncoder Just value_ -> justEncoder value_ ) |> variant0 Nothing |> variant1 Just justCodec |> finishCustomType {-| Codec for serializing a `List` import Serialize as S listOfStringsCodec : S.Codec (List String) listOfStringsCodec = S.list S.string -} list : Codec e a -> Codec e (List a) list codec = build (JE.list (getJsonEncoder codec)) (JD.list (getJsonDecoder codec) |> JD.map (List.foldr (\value state -> case ( value, state ) of ( Ok ok, Ok okState ) -> ok :: okState |> Ok ( _, Err _ ) -> state ( Err error, Ok _ ) -> Err error ) (Ok []) ) ) {-| Codec for serializing an `Array` -} array : Codec e a -> Codec e (Array a) array codec = list codec |> mapHelper (Result.map Array.fromList) Array.toList {-| Codec for serializing a `Dict` import Serialize as S type alias Name = String peoplesAgeCodec : S.Codec (Dict Name Int) peoplesAgeCodec = S.dict S.string S.int -} dict : Codec e comparable -> Codec e a -> Codec e (Dict comparable a) dict keyCodec valueCodec = list (tuple keyCodec valueCodec) |> mapHelper (Result.map Dict.fromList) Dict.toList {-| Codec for serializing a `Set` -} set : Codec e comparable -> Codec e (Set comparable) set codec = list codec |> mapHelper (Result.map Set.fromList) Set.toList {-| Codec for serializing `()` (aka `Unit`). -} unit : Codec e () unit = build (\_ -> JE.int 0) (JD.succeed (Ok ())) {-| Codec for serializing a tuple with 2 elements import Serialize as S pointCodec : S.Codec ( Float, Float ) pointCodec = S.tuple S.float S.float -} tuple : Codec e a -> Codec e b -> Codec e ( a, b ) tuple codecFirst codecSecond = record Tuple.pair |> field Tuple.first codecFirst |> field Tuple.second codecSecond |> finishRecord {-| Codec for serializing a tuple with 3 elements import Serialize as S pointCodec : S.Codec ( Float, Float, Float ) pointCodec = S.tuple S.float S.float S.float -} triple : Codec e a -> Codec e b -> Codec e c -> Codec e ( a, b, c ) triple codecFirst codecSecond codecThird = record (\a b c -> ( a, b, c )) |> field (\( a, _, _ ) -> a) codecFirst |> field (\( _, b, _ ) -> b) codecSecond |> field (\( _, _, c ) -> c) codecThird |> finishRecord {-| Codec for serializing a `Result` -} result : Codec e error -> Codec e value -> Codec e (Result error value) result errorCodec valueCodec = customType (\errEncoder okEncoder value -> case value of Err err -> errEncoder err Ok ok -> okEncoder ok ) |> variant1 Err errorCodec |> variant1 Ok valueCodec |> finishCustomType {-| A codec for serializing an item from a list of possible items. If you try to encode an item that isn't in the list then the first item is defaulted to. import Serialize as S type DaysOfWeek = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday daysOfWeekCodec : S.Codec DaysOfWeek daysOfWeekCodec = S.enum Monday [ Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday ] Note that inserting new items in the middle of the list or removing items is a breaking change. It's safe to add items to the end of the list though. -} enum : a -> List a -> Codec e a enum defaultItem items = let getIndex value = items |> findIndex ((==) value) |> Maybe.withDefault -1 |> (+) 1 getItem index = if index < 0 then Err DataCorrupted else if index > List.length items then Err DataCorrupted else getAt (index - 1) items |> Maybe.withDefault defaultItem |> Ok in build (getIndex >> JE.int) (JD.int |> JD.map getItem) getAt : Int -> List a -> Maybe a getAt idx xs = if idx < 0 then Nothing else List.head <| List.drop idx xs {-| -} findIndex : (a -> Bool) -> List a -> Maybe Int findIndex = findIndexHelp 0 {-| -} findIndexHelp : Int -> (a -> Bool) -> List a -> Maybe Int findIndexHelp index predicate list_ = case list_ of [] -> Nothing x :: xs -> if predicate x then Just index else findIndexHelp (index + 1) predicate xs -- OBJECTS {-| A partially built Codec for a record. -} type RecordCodec e a b = RecordCodec { jsonEncoder : a -> List JE.Value , jsonDecoder : JD.Decoder (Result (Error e) b) , fieldIndex : Int } {-| Start creating a codec for a record. import Serialize as S type alias Point = { x : Int , y : Int } pointCodec : S.Codec Point pointCodec = S.record Point -- Note that adding, removing, or reordering fields will prevent you from decoding any data you've previously encoded. |> S.field .x S.int |> S.field .y S.int |> S.finishRecord -} record : b -> RecordCodec e a b record ctor = RecordCodec { jsonEncoder = \_ -> [] , jsonDecoder = JD.succeed (Ok ctor) , fieldIndex = 0 } {-| Add a field to the record we are creating a codec for. -} field : (a -> f) -> Codec e f -> RecordCodec e a (f -> b) -> RecordCodec e a b field getter codec (RecordCodec recordCodec) = RecordCodec { jsonEncoder = \v -> (getJsonEncoder codec <| getter v) :: recordCodec.jsonEncoder v , jsonDecoder = JD.map2 (\f x -> case ( f, x ) of ( Ok fOk, Ok xOk ) -> fOk xOk |> Ok ( Err err, _ ) -> Err err ( _, Err err ) -> Err err ) recordCodec.jsonDecoder (JD.index recordCodec.fieldIndex (getJsonDecoder codec)) , fieldIndex = recordCodec.fieldIndex + 1 } {-| Finish creating a codec for a record. -} finishRecord : RecordCodec e a a -> Codec e a finishRecord (RecordCodec codec) = Codec { jsonEncoder = codec.jsonEncoder >> List.reverse >> JE.list identity , jsonDecoder = codec.jsonDecoder } -- CUSTOM {-| A partially built codec for a custom type. -} type CustomTypeCodec a e match v = CustomTypeCodec { jsonMatch : match , jsonDecoder : Int -> JD.Decoder (Result (Error e) v) -> JD.Decoder (Result (Error e) v) , idCounter : Int } {-| Starts building a `Codec` for a custom type. You need to pass a pattern matching function, see the FAQ for details. import Serialize as S type Semaphore = Red Int String Bool | Yellow Float | Green semaphoreCodec : S.Codec Semaphore semaphoreCodec = S.custom (\redEncoder yellowEncoder greenEncoder value -> case value of Red i s b -> redEncoder i s b Yellow f -> yellowEncoder f Green -> greenEncoder ) -- Note that removing a variant, inserting a variant before an existing one, or swapping two variants will prevent you from decoding any data you've previously encoded. |> S.variant3 Red S.int S.string S.bool |> S.variant1 Yellow S.float |> S.variant0 Green -- It's safe to add new variants here later though |> S.finishCustom -} customType : match -> CustomTypeCodec { youNeedAtLeastOneVariant : () } e match value customType match = CustomTypeCodec { jsonMatch = match , jsonDecoder = \_ -> identity , idCounter = 0 } {-| -} type VariantEncoder = VariantEncoder ( (), JE.Value ) variant : ((List JE.Value -> VariantEncoder) -> a) -> JD.Decoder (Result (Error error) v) -> CustomTypeCodec z error (a -> b) v -> CustomTypeCodec () error b v variant matchJsonPiece jsonDecoderPiece (CustomTypeCodec am) = let jsonEnc : List JE.Value -> VariantEncoder jsonEnc v = ( () , JE.int am.idCounter :: v |> JE.list identity ) |> VariantEncoder jsonDecoder_ : Int -> JD.Decoder (Result (Error error) v) -> JD.Decoder (Result (Error error) v) jsonDecoder_ tag orElse = if tag == am.idCounter then jsonDecoderPiece else am.jsonDecoder tag orElse in CustomTypeCodec { jsonMatch = am.jsonMatch <| matchJsonPiece jsonEnc , jsonDecoder = jsonDecoder_ , idCounter = am.idCounter + 1 } {-| Define a variant with 0 parameters for a custom type. -} variant0 : v -> CustomTypeCodec z e (VariantEncoder -> a) v -> CustomTypeCodec () e a v variant0 ctor = variant (\c -> c []) (JD.succeed (Ok ctor)) {-| Define a variant with 1 parameters for a custom type. -} variant1 : (a -> v) -> Codec error a -> CustomTypeCodec z error ((a -> VariantEncoder) -> b) v -> CustomTypeCodec () error b v variant1 ctor m1 = variant (\c v -> c [ getJsonEncoder m1 v ] ) (JD.map (result1 ctor) (JD.index 1 (getJsonDecoder m1))) result1 : (value -> a) -> Result error value -> Result error a result1 ctor value = case value of Ok ok -> ctor ok |> Ok Err err -> Err err {-| Define a variant with 2 parameters for a custom type. -} variant2 : (a -> b -> v) -> Codec error a -> Codec error b -> CustomTypeCodec z error ((a -> b -> VariantEncoder) -> c) v -> CustomTypeCodec () error c v variant2 ctor m1 m2 = variant (\c v1 v2 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 ] |> c ) (JD.map2 (result2 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) ) result2 : (value -> a -> b) -> Result error value -> Result error a -> Result error b result2 ctor v1 v2 = case ( v1, v2 ) of ( Ok ok1, Ok ok2 ) -> ctor ok1 ok2 |> Ok ( Err err, _ ) -> Err err ( _, Err err ) -> Err err {-| Define a variant with 3 parameters for a custom type. -} variant3 : (a -> b -> c -> v) -> Codec error a -> Codec error b -> Codec error c -> CustomTypeCodec z error ((a -> b -> c -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v variant3 ctor m1 m2 m3 = variant (\c v1 v2 v3 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 , getJsonEncoder m3 v3 ] |> c ) (JD.map3 (result3 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) (JD.index 3 (getJsonDecoder m3)) ) result3 : (value -> a -> b -> c) -> Result error value -> Result error a -> Result error b -> Result error c result3 ctor v1 v2 v3 = case ( v1, v2, v3 ) of ( Ok ok1, Ok ok2, Ok ok3 ) -> ctor ok1 ok2 ok3 |> Ok ( Err err, _, _ ) -> Err err ( _, Err err, _ ) -> Err err ( _, _, Err err ) -> Err err {-| Define a variant with 4 parameters for a custom type. -} variant4 : (a -> b -> c -> d -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> CustomTypeCodec z error ((a -> b -> c -> d -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v variant4 ctor m1 m2 m3 m4 = variant (\c v1 v2 v3 v4 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 , getJsonEncoder m3 v3 , getJsonEncoder m4 v4 ] |> c ) (JD.map4 (result4 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) (JD.index 3 (getJsonDecoder m3)) (JD.index 4 (getJsonDecoder m4)) ) result4 : (value -> a -> b -> c -> d) -> Result error value -> Result error a -> Result error b -> Result error c -> Result error d result4 ctor v1 v2 v3 v4 = case T4 v1 v2 v3 v4 of T4 (Ok ok1) (Ok ok2) (Ok ok3) (Ok ok4) -> ctor ok1 ok2 ok3 ok4 |> Ok T4 (Err err) _ _ _ -> Err err T4 _ (Err err) _ _ -> Err err T4 _ _ (Err err) _ -> Err err T4 _ _ _ (Err err) -> Err err {-| Define a variant with 5 parameters for a custom type. -} variant5 : (a -> b -> c -> d -> e -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v variant5 ctor m1 m2 m3 m4 m5 = variant (\c v1 v2 v3 v4 v5 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 , getJsonEncoder m3 v3 , getJsonEncoder m4 v4 , getJsonEncoder m5 v5 ] |> c ) (JD.map5 (result5 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) (JD.index 3 (getJsonDecoder m3)) (JD.index 4 (getJsonDecoder m4)) (JD.index 5 (getJsonDecoder m5)) ) result5 ctor v1 v2 v3 v4 v5 = case T5 v1 v2 v3 v4 v5 of T5 (Ok ok1) (Ok ok2) (Ok ok3) (Ok ok4) (Ok ok5) -> ctor ok1 ok2 ok3 ok4 ok5 |> Ok T5 (Err err) _ _ _ _ -> Err err T5 _ (Err err) _ _ _ -> Err err T5 _ _ (Err err) _ _ -> Err err T5 _ _ _ (Err err) _ -> Err err T5 _ _ _ _ (Err err) -> Err err {-| Define a variant with 6 parameters for a custom type. -} variant6 : (a -> b -> c -> d -> e -> f -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> Codec error f -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> f -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v variant6 ctor m1 m2 m3 m4 m5 m6 = variant (\c v1 v2 v3 v4 v5 v6 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 , getJsonEncoder m3 v3 , getJsonEncoder m4 v4 , getJsonEncoder m5 v5 , getJsonEncoder m6 v6 ] |> c ) (JD.map5 (result6 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) (JD.index 3 (getJsonDecoder m3)) (JD.index 4 (getJsonDecoder m4)) (JD.map2 Tuple.pair (JD.index 5 (getJsonDecoder m5)) (JD.index 6 (getJsonDecoder m6)) ) ) result6 : (value -> a -> b -> c -> d -> e -> f) -> Result error value -> Result error a -> Result error b -> Result error c -> ( Result error d, Result error e ) -> Result error f result6 ctor v1 v2 v3 v4 ( v5, v6 ) = case T6 v1 v2 v3 v4 v5 v6 of T6 (Ok ok1) (Ok ok2) (Ok ok3) (Ok ok4) (Ok ok5) (Ok ok6) -> ctor ok1 ok2 ok3 ok4 ok5 ok6 |> Ok T6 (Err err) _ _ _ _ _ -> Err err T6 _ (Err err) _ _ _ _ -> Err err T6 _ _ (Err err) _ _ _ -> Err err T6 _ _ _ (Err err) _ _ -> Err err T6 _ _ _ _ (Err err) _ -> Err err T6 _ _ _ _ _ (Err err) -> Err err {-| Define a variant with 7 parameters for a custom type. -} variant7 : (a -> b -> c -> d -> e -> f -> g -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> Codec error f -> Codec error g -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> f -> g -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v variant7 ctor m1 m2 m3 m4 m5 m6 m7 = variant (\c v1 v2 v3 v4 v5 v6 v7 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 , getJsonEncoder m3 v3 , getJsonEncoder m4 v4 , getJsonEncoder m5 v5 , getJsonEncoder m6 v6 , getJsonEncoder m7 v7 ] |> c ) (JD.map5 (result7 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) (JD.index 3 (getJsonDecoder m3)) (JD.map2 Tuple.pair (JD.index 4 (getJsonDecoder m4)) (JD.index 5 (getJsonDecoder m5)) ) (JD.map2 Tuple.pair (JD.index 6 (getJsonDecoder m6)) (JD.index 7 (getJsonDecoder m7)) ) ) result7 : (value -> a -> b -> c -> d -> e -> f -> g) -> Result error value -> Result error a -> Result error b -> ( Result error c, Result error d ) -> ( Result error e, Result error f ) -> Result error g result7 ctor v1 v2 v3 ( v4, v5 ) ( v6, v7 ) = case T7 v1 v2 v3 v4 v5 v6 v7 of T7 (Ok ok1) (Ok ok2) (Ok ok3) (Ok ok4) (Ok ok5) (Ok ok6) (Ok ok7) -> ctor ok1 ok2 ok3 ok4 ok5 ok6 ok7 |> Ok T7 (Err err) _ _ _ _ _ _ -> Err err T7 _ (Err err) _ _ _ _ _ -> Err err T7 _ _ (Err err) _ _ _ _ -> Err err T7 _ _ _ (Err err) _ _ _ -> Err err T7 _ _ _ _ (Err err) _ _ -> Err err T7 _ _ _ _ _ (Err err) _ -> Err err T7 _ _ _ _ _ _ (Err err) -> Err err {-| Define a variant with 8 parameters for a custom type. -} variant8 : (a -> b -> c -> d -> e -> f -> g -> h -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> Codec error f -> Codec error g -> Codec error h -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> f -> g -> h -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v variant8 ctor m1 m2 m3 m4 m5 m6 m7 m8 = variant (\c v1 v2 v3 v4 v5 v6 v7 v8 -> [ getJsonEncoder m1 v1 , getJsonEncoder m2 v2 , getJsonEncoder m3 v3 , getJsonEncoder m4 v4 , getJsonEncoder m5 v5 , getJsonEncoder m6 v6 , getJsonEncoder m7 v7 , getJsonEncoder m8 v8 ] |> c ) (JD.map5 (result8 ctor) (JD.index 1 (getJsonDecoder m1)) (JD.index 2 (getJsonDecoder m2)) (JD.map2 Tuple.pair (JD.index 3 (getJsonDecoder m3)) (JD.index 4 (getJsonDecoder m4)) ) (JD.map2 Tuple.pair (JD.index 5 (getJsonDecoder m5)) (JD.index 6 (getJsonDecoder m6)) ) (JD.map2 Tuple.pair (JD.index 7 (getJsonDecoder m7)) (JD.index 8 (getJsonDecoder m8)) ) ) result8 : (value -> a -> b -> c -> d -> e -> f -> g -> h) -> Result error value -> Result error a -> ( Result error b, Result error c ) -> ( Result error d, Result error e ) -> ( Result error f, Result error g ) -> Result error h result8 ctor v1 v2 ( v3, v4 ) ( v5, v6 ) ( v7, v8 ) = case T8 v1 v2 v3 v4 v5 v6 v7 v8 of T8 (Ok ok1) (Ok ok2) (Ok ok3) (Ok ok4) (Ok ok5) (Ok ok6) (Ok ok7) (Ok ok8) -> ctor ok1 ok2 ok3 ok4 ok5 ok6 ok7 ok8 |> Ok T8 (Err err) _ _ _ _ _ _ _ -> Err err T8 _ (Err err) _ _ _ _ _ _ -> Err err T8 _ _ (Err err) _ _ _ _ _ -> Err err T8 _ _ _ (Err err) _ _ _ _ -> Err err T8 _ _ _ _ (Err err) _ _ _ -> Err err T8 _ _ _ _ _ (Err err) _ _ -> Err err T8 _ _ _ _ _ _ (Err err) _ -> Err err T8 _ _ _ _ _ _ _ (Err err) -> Err err {-| Finish creating a codec for a custom type. -} finishCustomType : CustomTypeCodec () e (a -> VariantEncoder) a -> Codec e a finishCustomType (CustomTypeCodec am) = build (am.jsonMatch >> (\(VariantEncoder ( _, a )) -> a)) (JD.index 0 JD.int |> JD.andThen (\tag -> am.jsonDecoder tag (JD.succeed (Err DataCorrupted)) ) ) ---- MAPPING {-| Map from one codec to another codec import Serialize as S type UserId = UserId Int userIdCodec : S.Codec UserId userIdCodec = S.int |> S.map UserId (\(UserId id) -> id) Note that there's nothing preventing you from encoding Elm values that will map to some different value when you decode them. I recommend writing tests for Codecs that use `map` to make sure you get back the same Elm value you put in. [Here's some helper functions to get you started.](https://github.com/MartinSStewart/elm-geometry-serialize/blob/6f2244c28631ede1b864cb43541d1573dc628904/tests/Tests.elm#L49-L74) -} map : (a -> b) -> (b -> a) -> Codec e a -> Codec e b map fromBytes_ toBytes_ codec = mapHelper (\value -> case value of Ok ok -> fromBytes_ ok |> Ok Err err -> Err err ) toBytes_ codec mapHelper : (Result (Error e) a -> Result (Error e) b) -> (b -> a) -> Codec e a -> Codec e b mapHelper fromBytes_ toBytes_ codec = build (\v -> toBytes_ v |> getJsonEncoder codec) (getJsonDecoder codec |> JD.map fromBytes_) {-| Map from one codec to another codec in a way that can potentially fail when decoding. -- Email module is from https://package.elm-lang.org/packages/tricycle/elm-email/1.0.2/ import Email import Serialize as S emailCodec : S.Codec String Float emailCodec = S.string |> S.mapValid (\text -> case Email.fromString of Just email -> Ok email Nothing -> Err "Invalid email" ) Email.toString Note that there's nothing preventing you from encoding Elm values that will produce Err when you decode them. I recommend writing tests for Codecs that use `mapValid` to make sure you get back the same Elm value you put in. [Here's some helper functions to get you started.](https://github.com/MartinSStewart/elm-geometry-serialize/blob/6f2244c28631ede1b864cb43541d1573dc628904/tests/Tests.elm#L49-L74) -} mapValid : (a -> Result e b) -> (b -> a) -> Codec e a -> Codec e b mapValid fromBytes_ toBytes_ codec = build (\v -> toBytes_ v |> getJsonEncoder codec) (getJsonDecoder codec |> JD.map (\value -> case value of Ok ok -> fromBytes_ ok |> Result.mapError CustomError Err err -> Err err ) ) {-| Map errors generated by `mapValid`. -} mapError : (e1 -> e2) -> Codec e1 a -> Codec e2 a mapError mapFunc codec = build (getJsonEncoder codec) (getJsonDecoder codec |> JD.map (mapErrorHelper mapFunc)) mapErrorHelper : (e -> a) -> Result (Error e) b -> Result (Error a) b mapErrorHelper mapFunc = Result.mapError (\error -> case error of CustomError custom -> mapFunc custom |> CustomError DataCorrupted -> DataCorrupted SerializerOutOfDate -> SerializerOutOfDate ) -- STACK UNSAFE {-| Handle situations where you need to define a codec in terms of itself. import Serialize as S type Peano = Peano (Maybe Peano) {-| The compiler will complain that this function causes an infinite loop. -} badPeanoCodec : S.Codec Peano badPeanoCodec = S.maybe badPeanoCodec |> S.map Peano (\(Peano a) -> a) {-| Now the compiler is happy! -} goodPeanoCodec : S.Codec Peano goodPeanoCodec = S.maybe (S.lazy (\() -> goodPeanoCodec)) |> S.map Peano (\(Peano a) -> a) **Warning:** This is not stack safe. In general if you have a type that contains itself, like with our the Peano example, then you're at risk of a stack overflow while decoding. Even if you're translating your nested data into a list before encoding, you're at risk, because the function translating back after decoding can cause a stack overflow if the original value was nested deeply enough. Be careful here, and test your codecs using elm-test with larger inputs than you ever expect to see in real life. -} lazy : (() -> Codec e a) -> Codec e a lazy f = build (\value -> getJsonEncoder (f ()) value) (JD.succeed () |> JD.andThen (\() -> getJsonDecoder (f ())))