lue-bird / elm-morph / Value.Morph

Morph your types over a generic, case-able elm value


type alias MorphValue narrow =
MorphIndependently (Value Value.IndexOrName -> Result Morph.Error narrow) (narrow -> Value Value.IndexAndName)

Morph between a given narrow value and a generic value

The way it's set up, it allows build up tags as both IndexAndName which means you can sometimes choose which one yuo want to display for a more broad format. See eachTag

unit : MorphValue ()

() MorphValue

Used in when morphing a variant with 0 attached values.

grouping


type alias MorphValueGroupEmptiable noPartPossiblyOrNever groupNarrow groupNarrowFurther =
Morph.PartsMorphEmptiable noPartPossiblyOrNever (Value.Record Value.IndexOrName -> Result PartsError groupNarrowFurther) (groupNarrow -> Value.Record Value.IndexAndName)

possibly incomplete step from and to a Record

building:


type PartsError
    = TagsMissing (Emptiable (Stacked Basics.Int) Basics.Never)
    | ValueError ({ index : Basics.Int, error : Morph.Error })

What can go wrong while narrowing to a Record

group : groupNarrowAssemble -> MorphValueGroupEmptiable Possibly groupNarrow_ groupNarrowAssemble

Start assembling multiple parts.

Continue with |> part and after you're done, |> groupFinish.

An example translated from elm/json

import Decimal exposing (Decimal)
import Decimal.Morph
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)
import String.Morph
import Value

type alias Cause =
    -- from lue-bird/elm-no-record-type-alias-constructor-function
    RecordWithoutConstructorFunction
        { name : String
        , percent : Decimal
        , per100k : Decimal
        }

value : MorphValue Cause
value =
    Value.Morph.group
        (\name percent per100k ->
            { name = name, percent = percent, per100k = per100k }
        )
        |> Value.Morph.part ( .name, "name" ) String.Morph.value
        |> Value.Morph.part ( .percent, "percent" ) Decimal.Morph.value
        |> Value.Morph.part ( .per100k, "per100k" ) Decimal.Morph.value
        |> Value.Morph.groupFinish

Another example for tuples

{-| `( ..., ... )` `MorphValue`

Just use a record with descriptive names instead!

-}
tuple2 :
    ( Morph part0
    , Morph part1
    )
    -> Morph ( part0, part1 )
tuple2 ( part0Morph, part1Morph ) =
    Morph.named "2-tuple"
        (Value.Morph.group
            (\part0 part1 -> ( part0, part1 ))
            |> Value.Morph.part ( Tuple.first, "part0" ) part0Morph
            |> Value.Morph.part ( Tuple.second, "part1" ) part1Morph
            |> Value.Morph.groupFinish
        )

{-| `( ..., ..., ... )` `MorphValue`

Just use a record with descriptive names instead!

-}
tuple3 :
    ( Morph part0
    , Morph part1
    , Morph part2
    )
    -> Morph ( part0, part1, part2 )
tuple3 ( part0Morph, part1Morph, part2Morph ) =
    Morph.named "3-tuple"
        (Value.Morph.group
            (\part0 part1 part2 -> ( part0, part1, part2 ))
            |> Value.Morph.part ( \( part0, _, _ ) -> part0, "part0" ) part0Morph
            |> Value.Morph.part ( \( _, part1, _ ) -> part1, "part1" ) part1Morph
            |> Value.Morph.part ( \( _, _, part2 ) -> part2, "part2" ) part2Morph
            |> Value.Morph.groupFinish
        )

part : ( group -> partValueNarrow, String ) -> MorphValue partValueNarrow -> MorphValueGroupEmptiable noPartPossiblyOrNever_ group (partValueNarrow -> groupNarrowFurther) -> MorphValueGroupEmptiable noPartNever_ group groupNarrowFurther

Continue a group assembly MorphValue.

For example to morph a name String field, add

|> Value.Morph.part ( .name, "name" ) String.Morph.value

Once you've assembled all parts, end the builder with groupFinish.

groupFinish : MorphValueGroupEmptiable Basics.Never record record -> MorphValue record

Conclude the Value.Morph.group |> Value.Morph.part builder

choice

variant union MorphValue

variant : ( possibilityNarrow -> choiceNarrow, String ) -> MorphValue possibilityNarrow -> Morph.ChoiceMorphEmptiable noTryPossiblyOrNever_ choiceNarrow (Value.Tagged Value.IndexOrName) ((possibilityNarrow -> Value.Tagged Value.IndexAndName) -> choiceToBroadFurther) Morph.Error -> Morph.ChoiceMorphEmptiable noTryNever_ choiceNarrow (Value.Tagged Value.IndexOrName) choiceToBroadFurther Morph.Error

Describe another variant MorphValue

When you're done, end the builder with |> Value.Morph.choiceFinish

type User
    = Guest GuestData
    | SignedIn SignedInData

value : MorphValue User
value =
    Morph.choice
        (\variantGuest variantSignedIn user ->
            case user of
                Guest guest ->
                    variantGuest guest

                SignedIn signedIn ->
                    variantSignedIn signedIn
        )
        |> Value.Morph.variant ( Guest, "guest" ) guestValue
        |> Value.Morph.variant ( SignedIn, "signed in" ) signedInValue
        |> Value.Morph.choiceFinish

where guestValue : MorphValue GuestData and signedInValue : MorphValue SignedInData.

If a variant has no attached thing, use Value.Morph.unit

signValue : MorphValue Sign
signValue =
    Morph.choice
        (\positive negative sign ->
            case sign of
                Positive ->
                    positive ()

                Negative ->
                    negative ()
        )
        |> Value.Morph.variant ( \() -> Positive, "Positive" ) Value.Morph.unit
        |> Value.Morph.variant ( \() -> Negative, "Negative" ) Value.Morph.unit
        |> Value.Morph.choiceFinish

choiceFinish : Morph.ChoiceMorphEmptiable Basics.Never choiceNarrow (Value.Tagged Value.IndexOrName) (choiceNarrow -> Value.Tagged Value.IndexAndName) Morph.Error -> MorphValue choiceNarrow

Conclude a Morph.choice |> Value.Morph.variant builder.

tag

eachTag : MorphIndependently (tagBeforeMap -> Result (Morph.ErrorWithDeadEnd Basics.Never) tagMapped) (tagBeforeUnmap -> tagUnmapped) -> MorphIndependently (Value tagBeforeMap -> Result (Morph.ErrorWithDeadEnd never_) (Value tagMapped)) (Value tagBeforeUnmap -> Value tagUnmapped)

Morph.OneToOne the tags of a Value.

The simplest such tag morph is descriptive, which is simply

Morph.oneToOne Value.Name .name

We keep only the .name and we reconstruct any tag as a name.

A more complex example is Json.Morph.compact

Morph.oneToOne
    (\tag ->
        case tag |> String.uncons of
            Just ( 'a', tagAfterA ) ->
                case tagAfterA |> String.toInt of
                    Just index ->
                        index |> Value.Index
                    ...
    )
    (\tag -> "a" ++ (tag.index |> String.fromInt))

We keep the .index but prefixed with "a" to make it a field name. When recovering, we have to drop the "a" and then extract the index again. For all the cases where extracting fails, it's usually nice to just return them as a Value.Name since we do actually know how to decode only based on name.

An example chain that uses eachTag to decode a project to a compact Json.Encode.Value:

Project.Morph.value
    |> Morph.over (Value.Morph.eachTag Json.Morph.compact)
    |> Morph.over Value.Morph.json
    |> Morph.over Json.Morph.jsValueMagic

descriptive : MorphIndependently (String -> Result error_ Value.IndexOrName) (Value.IndexAndName -> String)

with readable names. Use in combination with eachTag

Another option to morph tags is Json.Morph.compact

spin your own

build on existing ones

{-| `Posix` `MorphValue`
-}
posixValue : MorphValue Posix
posixValue =
    Morph.named "posix"
        (Morph.oneToOne
            Time.posixToMillis
            Time.millisToPosix
            |> Morph.over Int.Morph.integer
            |> Morph.over Integer.Morph.value
        )

or define new atoms, composed structures, ... (↓ are used by Json for example)

toAtom : MorphIndependently (Value.AtomOrComposed narrowAtom narrowComposed_ -> Result Morph.Error narrowAtom) (broadAtom -> Value.AtomOrComposed broadAtom broadComposed_)

Morph to a AtomOrComposed's atom if possible

toComposed : MorphIndependently (Value.AtomOrComposed narrowAtom_ narrowComposed -> Result Morph.Error narrowComposed) (broadComposed -> Value.AtomOrComposed broadAtom_ broadComposed)

Morph to a AtomOrComposed's composed if possible

supported default broad formats

bits : MorphRowIndependently (Value Value.IndexOrName) (Value Value.IndexAndName) Bit

MorphRow from Bits.

Example chain converting to Bytes

yourTypeMorphValue
    |> Morph.overRow Value.Morph.bits
    |> Morph.over List.Morph.bytes