This package allows you to compose "tools", where a tool is any Elm package with an API similar to miniBill/elm-codec
and edkelly303/elm-any-type-forms.
Imagine you have some complex type, like:
type alias User =
{ name : String
, age : Int
, role : Role
}
type Role
= Regular
| AdminLevel IntYou want to use edkelly303/elm-any-type-forms to build a form that will allow you to create/update values of your
User type. You also want to use miniBill/elm-codec to generate JSON encoders and decoders for the User type.
Without Elm MultiTool, you have to define your codec and form control separately, which means writing almost exactly the same code twice:
import Codec
import Control
userCodec =
Codec.object User
|> Codec.field "name" .name Codec.string
|> Codec.field "age" .age Codec.int
|> Codec.field "role" .role roleCodec
|> Codec.buildObject
userControl =
Control.record User
|> Control.field "name" .name Control.string
|> Control.field "age" .age Control.int
|> Control.field "role" .role roleControl
|> Control.end
roleCodec =
Codec.custom
(\regular adminLevel tag ->
case tag of
Regular ->
regular
AdminLevel level ->
adminLevel level
)
|> Codec.variant0 "Regular" Regular
|> Codec.variant1 "AdminLevel" AdminLevel Codec.int
|> Codec.buildCustom
roleControl =
Control.customType
(\regular adminLevel tag ->
case tag of
Regular ->
regular
AdminLevel level ->
adminLevel level
)
|> Control.tag0 "Regular" Regular
|> Control.tag1 "AdminLevel" AdminLevel Control.int
|> Control.endIf you want to create controls and codecs for many types across your codebase, this could get tedious.
With Elm MultiTool, by contrast, you do a little bit of initial setup to define the tools you want to use in your application:
import Tools.Codec
import Tools.Control
type alias Tools codec control =
{ codec : codec, control : control }
tools =
-- The next line is not a typo - you *do* need to
-- pass `Tools` twice. If anyone can figure out
-- a way to make this work by only passing it once,
-- please let me know!
MultiTool.define Tools Tools
|> MultiTool.add .codec Tools.Codec.interface
|> MultiTool.add .control Tools.Control.interface
|> MultiTool.endAnd then you only need to write out the boilerplate once to create tool specifications (ToolSpecs) for each of your
types:
userToolSpec =
tools.record User
|> tools.field "name" .name tools.string
|> tools.field "age" .age tools.int
|> tools.field "role" .role roleToolSpec
|> tools.endRecord
roleToolSpec =
let
match regular adminLevel tag =
case tag of
Regular ->
regular
AdminLevel level ->
adminLevel level
in
tools.custom
-- We need to define the `match` function using
-- let-polymorphism and then pass it to each of our tools
-- separately. If anyone can figure out a way to make
-- this work without having to pass the function multiple
-- times, please let me know!
{ codec = match, control = match }
|> tools.tag0 "Regular" Regular
|> tools.tag1 "AdminLevel" AdminLevel tools.int
|> tools.endCustomNow, to use your tools, you can just do:
userTools =
tools.build userToolSpec
control =
userTools.control -- this is identical to userControl
codec =
userTools.codec -- this is identical to userCodecIf you only want JSON codecs and form controls for one small type, maybe not.
But if you also want bytes codecs, test fuzzers, random generators, toStrings, toComparables, and so on for many of the types in your application, it could save a lot of time and trouble.
Sure! I've implemented basic (possibly quite buggy) versions of interfaces for a variety of tools:
Tools.ToString will convert your Elm value to a StringTools.ToComparable will convert your Elm value to a comparable (specifically, a List String)Tools.Fuzz creates a fuzzer for use with elm-explorations/testTools.Random creates a random generator for use with elm/randomFrom the root folder, you can run the examples like this, using Simon Lydell's wonderful elm-watch tool:
$ cd examples
$ npm i
$ . run.shIf you were using elm-any-type-forms directly, you could do something like this to customise a string control:
myStringControl =
Control.string
|> Control.debounce 1000
|> Control.failIf
String.isEmpty
"Field can't be blank"With Elm MultiTool, you can achieve the same thing using the handy tweak functions:
myStringSpec =
tools.string
|> tools.tweak.control
(Control.debounce 1000)
|> tools.tweak.control
(Control.failIf
String.isEmpty
"Field can't be blank"
)It's similar to a map function, so you can use it to transform (or even replace) individual tools within a ToolSpec:
myStringSpec =
tools.string
|> tools.tweak.control
(\_ ->
-- throw away whatever control we
-- had in tools.string, and use
-- this instead:
Control.string
|> Control.debounce 1000
)