lue-bird / elm-review-missing-record-field-lens / VariantHelper.GenerateUsed

Generate helpers for variant values

rule : { nameInModuleInternal : VariantHelperNameConfig, nameInModuleExternal : VariantHelperNameConfig, build : VariantHelperBuild, generationModuleIsVariantModuleDotSuffix : String } -> Review.Rule.Rule

Generate each helper for a variant of a type that is called from your code but isn't already defined.

import Review.Rule as Rule exposing (Rule)
import VariantHelper.GenerateUsed

config : List Rule
config =
    [ VariantHelper.GenerateUsed.rule ..config..
    ]

..config.. How to generate, where to generate:

There's no configuration to automatically import Variant.Module.Generation as Variant.Module because import aliases can't contain .

example module Variant.Module.On exposing (some)

{ build =
    VariantHelper.GenerateUsed.accessors
        { valuesCombined = VariantHelper.GenerateUsed.valuesRecord }
, nameInModuleInternal = VariantHelper.GenerateUsed.variant
, nameInModuleExternal = VariantHelper.GenerateUsed.variantAfter "on"
, generationModuleIsVariantModuleDotSuffix = "On"
}

example: module Variant.Module.X exposing (onSome)

{ build =
    VariantHelper.GenerateUsed.accessors
        { valuesCombined = VariantHelper.GenerateUsed.valuesRecord }
, nameInModuleInternal = VariantHelper.GenerateUsed.variantAfter "on"
, nameInModuleExternal = VariantHelper.GenerateUsed.variantAfter "on"
, generationModuleIsVariantModuleDotSuffix = "X"
}

Variant helpers will be generated below the type declaration. Consider SiriusStarr/elm-review-no-unsorted for more fine-grained and consistent positioning control

use it

... when you're using elm-accessors to mitigate boilerplate related to updating potentially deeply nested data.

don't use it

... when you consider accessors the less readable/intuitive/simple/explicit alternative.

build


type alias VariantHelperBuild =
{ variantModule : String
, typeName : String
, typeParameters : List String
, variantName : String
, variantValues : List Elm.CodeGen.TypeAnnotation
, otherVariants : Dict String { valueCount : Basics.Int } } -> { imports : List Elm.CodeGen.Import
, documentation : Maybe (Elm.CodeGen.Comment Elm.CodeGen.DocComment)
, annotation : Maybe Elm.CodeGen.TypeAnnotation
, implementation : Elm.CodeGen.Expression 
}

Configure how to generate a variant helper declaration plus the necessary imports.

Out of the box, there are

Customize with

Create a custom helper generator (or just parts for replacement) with

You can use the source code of accessors & co. as a starting point.

accessors : { valuesCombined : ValuesCombined } -> VariantHelperBuild

Build of named erlandsona/elm-accessors which with

{ build =
    VariantHelper.GenerateUsed.accessors
        { valuesCombined = VariantHelper.GenerateUsed.valuesTupleNest }
, nameInModuleInternal = VariantHelper.GenerateUsed.variantAfter "on"
, nameInModuleExternal = VariantHelper.GenerateUsed.variant
, generationModuleIsVariantModuleDotSuffix = "On"
}

and

module Data exposing (Data(..))

type Data a b c d
    = Some a b c d
    | None

generates

module Data.On exposing (some)

import Accessors exposing (makeOneToN_)
import Data exposing (Data(..))

{-| Accessor prism for the variant `Data.Some` of the `type Data`.
-}
some :
    Relation ( a, ( b, ( c, d ) ) ) reachable wrap
    -> Relation (Data a b c d) reachable (Maybe wrap)
some =
    makeOneToN_
        "Data.Some"
        (\valuesAlter variantType ->
            case variantType of
                Some value0 value1 value2 value3 ->
                    ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter |> Just

                _ ->
                    Nothing
        )
        (\valuesAlter variantType ->
            case variantType of
                Some value0 value1 value2 value3 ->
                    let
                        ( alteredValue0, ( alteredValue1, ( alteredValue2, alteredValue3 ) ) ) =
                            ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter
                    in
                    Some alteredValue0 alteredValue1 alteredValue2 alteredValue3

                other ->
                    other
        )

accessorsBChiquet : { valuesCombined : ValuesCombined } -> VariantHelperBuild

Build of unnamed bChiquet/elm-accessors which with

{ build =
    VariantHelper.GenerateUsed.accessorsBChiquet
        { valuesCombined = VariantHelper.GenerateUsed.valuesTupleNest }
, nameInModuleInternal = VariantHelper.GenerateUsed.variant
, nameInModuleExternal = VariantHelper.GenerateUsed.variantAfter "on"
, generationModuleIsVariantModuleDotSuffix = "On"
}

and

module Data exposing (Data(..))

type Data a b c d
    = Some a b c d
    | None

generates

module Data.On exposing (some)

import Accessors exposing (Lens, Relation, makeOneToN_)
import Data exposing (Data(..))

{-| Accessor prism for the variant `Data.Some` of the `type Data`.
-}
some :
    Relation ( a, ( b, ( c, d ) ) ) reachable wrap
    -> Relation (Data a b c d) reachable (Maybe wrap)
some =
    makeOneToN
        (\valuesAlter variantType ->
            case variantType of
                Some value0 value1 value2 value3 ->
                    ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter |> Just

                _ ->
                    Nothing
        )
        (\valuesAlter variantType ->
            case variantType of
                Some value0 value1 value2 value3 ->
                    let
                        ( alteredValue0, ( alteredValue1, ( alteredValue2, alteredValue3 ) ) ) =
                            ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter
                    in
                    Some alteredValue0 alteredValue1 alteredValue2 alteredValue3

                other ->
                    other
        )

documented : Elm.CodeGen.Comment Elm.CodeGen.DocComment -> { declaration | documentation : Maybe (Elm.CodeGen.Comment Elm.CodeGen.DocComment) } -> { declaration | documentation : Maybe (Elm.CodeGen.Comment Elm.CodeGen.DocComment) }

Build a different documentation:

accessorsDocumentedCustom info =
    accessors
        { valuesCombined = valuesRecord }
        info
        |> documented
            (emptyDocComment
                |> markdown
                    ("variant `" ++ info.variantName ++ "`: Accessor for the values.")
            )

annotated : Elm.CodeGen.TypeAnnotation -> { declaration | annotation : Maybe Elm.CodeGen.TypeAnnotation } -> { declaration | annotation : Maybe Elm.CodeGen.TypeAnnotation }

Build a different type annotation:

import Hand exposing (Hand(..))
import Stack

accessorsAnnotatedOption : Build
accessorsAnnotatedOption info =
    accessors
        { valuesCombined = valuesRecord }
        info
        |> annotated
            (typed "Option"
                [ CodeGen.typed info.typeName
                    (info.typeParameters |> List.map CodeGen.typeVar)
                , case variantValues |> Stack.fromList of
                    Empty _ ->
                        CodeGen.unitAnn

                    Filled stacked ->
                        Filled stacked
                            |> Stack.reverse
                            |> Stack.fold (\value soFar -> CodeGen.tupleAnn [ value, soFar ])
                , CodeGen.typeVar "reachable"
                , CodeGen.typeVar "wrap"
                ]
            )
        |> importsAdd
            [ impostStmt [ "Accessors" ]
                Nothing
                ([ "Option" |> typeOrAliasExpose ] |> exposingExplicit |> Just)
            ]

Make sure to importsAdd.

importsAdd : List Elm.CodeGen.Import -> { declaration | imports : List Elm.CodeGen.Import } -> { declaration | imports : List Elm.CodeGen.Import }

Supply additional imports required for generating the declaration.

accessorsAnnotatedOption : Build
accessorsAnnotatedOption info =
    accessors info
        |> annotated (typed "Option" [ ... ])
        |> importsAdd
            [ impostStmt [ "Accessors" ]
                Nothing
                ([ "Option" |> typeOrAliasExpose ] |> exposingExplicit |> Just)
            ]

variantInMultiple : { name : String, values : List Elm.CodeGen.TypeAnnotation, valuesCombined : ValuesCombined } -> { access : Elm.CodeGen.Expression, alter : Elm.CodeGen.Expression, typeValues : Elm.CodeGen.TypeAnnotation }

Helpers for values of a given variant among >= 2.

for

type Data a b c d
    = Some a b c d
    | None

with

variantInMultiple valuesTupleNest

access

\valuesAlter variantType ->
    case variantType of
        Some value0 value1 value2 value3 ->
            ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter |> Just

        _ ->
            Nothing

alter

\valuesAlter variantType ->
    case variantType of
        Some value0 value1 value2 value3 ->
            let
                ( alteredValue0, ( alteredValue1, ( alteredValue2, alteredValue3 ) ) ) =
                    ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter
            in
            Some alteredValue0 alteredValue1 alteredValue2 alteredValue3

        other ->
            other

variantOnly : { name : String, values : List Elm.CodeGen.TypeAnnotation, valuesCombined : ValuesCombined } -> { access : Elm.CodeGen.Expression, alter : Elm.CodeGen.Expression, typeValues : Elm.CodeGen.TypeAnnotation }

Helpers for values of a given only variant.

for

type Id attachment
    = Id (List Int) attachment

access

\(Id value0 value1) -> ( value0, value1 )

alter

\valuesAlter (Id value0 value1) ->
    let
        ( alteredValue0, alteredValue1 ) =
            ( value0, value1 ) |> valuesAlter
    in
    Id alteredValue0 alteredValue1


type alias ValuesCombined =
{ name : String
, values : List Elm.CodeGen.TypeAnnotation } -> { typeInOne : Elm.CodeGen.TypeAnnotation
, inOne : String -> Elm.CodeGen.Expression
, patternInOne : String -> Elm.CodeGen.Pattern
, alter : Elm.CodeGen.Expression 
}

Representation of values as one whole, for example

valuesTupleNest : { name : String, values : List Elm.CodeGen.TypeAnnotation } -> { typeInOne : Elm.CodeGen.TypeAnnotation, inOne : String -> Elm.CodeGen.Expression, patternInOne : String -> Elm.CodeGen.Pattern, alter : Elm.CodeGen.Expression }

Helpers for a given variant to use with a custom implementation or variantOnly/variantInMultiple.

for

type Data a b c d
    = Some a b c d
    | None

typeInOne

( a, ( b, ( c, d ) ) )

inOne "value"

( value0, ( value1, ( value2, value3 ) ) )

patternInOne "value"

( value0, ( value1, ( value2, value3 ) ) )

alter

let
    ( alteredValue0, ( alteredValue1, ( alteredValue2, alteredValue3 ) ) ) =
        ( value0, ( value1, ( value2, value3 ) ) ) |> valuesAlter
in
Some alteredValue0 alteredValue1 alteredValue2 alteredValue3

valuesRecord : { name : String, values : List Elm.CodeGen.TypeAnnotation } -> { typeInOne : Elm.CodeGen.TypeAnnotation, inOne : String -> Elm.CodeGen.Expression, patternInOne : String -> Elm.CodeGen.Pattern, alter : Elm.CodeGen.Expression }

Helpers for a given variant to use with a custom implementation or variantOnly/variantInMultiple.

for

type Data a b c d
    = Some a b c d
    | None

access

{ value0 = value0, value1 = value1, value2 = value2, value3 = value3 }

alter

let
    altered =
        { value0 = value0, value1 = value1, value2 = value2, value3 = value3 }
            |> valuesAlter
in
Some altered.value0 altered.value1 altered.value2 altered.value3

typeValues

{ value0 = a, value1 = b, value2 = c, value3 = d }

variantPattern : { name : String, values : List Elm.CodeGen.TypeAnnotation } -> Elm.CodeGen.Pattern

Pattern on a given variant to use with a custom implementation.

for

type Data a b c d
    = Some a b c d
    | None

generates

Some value0 value1 value2 value3

name


type alias VariantHelperNameConfig =
RecordWithoutConstructorFunction { parser : Parser { variantName : String }
, build : { variantName : String } -> String 
}

How to derive helper name ↔ variant name.

Out of the box, there are

You can also create a custom VariantHelperNameConfig:

import Parser

{ build = \{ variantName } -> variantName ++ "Variant"
, parser =
    Parser.map (\variantName -> { variantName = variantName })
        (Parser.loop ""
            (\beforeSuffixFoFar ->
                Parser.oneOf
                    [ Parser.token "Variant"
                        |. Parser.end
                        |> Parser.map (\() -> Parser.Done beforeSuffixFoFar)
                    , Parser.chompIf (\_ -> True)
                        |> Parser.getChompedString
                        |> Parser.map
                            (\stillNotSuffix ->
                                Parser.Loop (beforeSuffixFoFar ++ stillNotSuffix)
                            )
                    ]
            )
        )
}

It's not half as daunting as it looks. If you feel motivated 👀 ↓

Mini tip: testing is always a good idea for Parsers

Don't worry about the casing of the results. They will be automatically be corrected when passed to rule.

In the future, elm-morph will allow creating builders and parsers in one go, making this easier.

variantAfter : String -> VariantHelperNameConfig

Handle helper names in the format prefix<Variant>. Check out VariantHelperNameConfig for all naming options.

import Parser
import VariantHelper.GenerateUsed

"toSuccess"
    |> Parser.run
        (VariantHelper.GenerateUsed.variantAfter "to"
            |> .parser
        )
--> { variantName = "Success" }

{ variantName = "Success" }
    |> (VariantHelper.GenerateUsed.variantAfter "map"
            |> .build
       )
--> "mapSuccess"

variant : VariantHelperNameConfig

Handle helper names in the format <variant>. Check out VariantHelperNameConfig for all naming options.

import Parser
import VariantHelper.GenerateUsed

"success"
    |> Parser.run VariantHelper.GenerateUsed.helperNameAsVariant.parser
--> { variantName = "Success" }

{ variantName = "Success" }
    |> VariantHelper.GenerateUsed.helperNameAsVariant.build
--> "success"

deprecated

onVariant : VariantHelperNameConfig

@deprecated in favor of variantAfter "on"

Handle helper names in the format on<Variant>.

Check out VariantHelperNameConfig for all naming options