packages feed

ditto 0.4.1 → 0.5.0

raw patch · 12 files changed

+478/−104 lines, 12 filesdep +dittodep −containersdep ~basedep ~mtldep ~textsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: ditto

Dependencies removed: containers

Dependency ranges changed: base, mtl, text

API changes (from Hackage documentation)

- Ditto.Backend: type family FileType input;
- Ditto.Core: instance (Ditto.Core.Environment m input, GHC.Base.Monoid view, Ditto.Backend.FormError input err) => GHC.Base.Monad (Ditto.Core.Form m input err view)
- Ditto.Core: instance GHC.Base.Applicative m => GHC.Base.Applicative (Ditto.Core.NoEnvironment input m)
- Ditto.Core: instance GHC.Base.Functor m => GHC.Base.Functor (Ditto.Core.NoEnvironment input m)
- Ditto.Core: instance GHC.Base.Monad m => GHC.Base.Monad (Ditto.Core.NoEnvironment input m)
+ Ditto.Backend: type FileType input;
+ Ditto.Core: hoistForm :: (forall x. () => m x -> f x) -> Form m input err view a -> Form f input err view a
+ Ditto.Core: instance (Ditto.Core.Environment m input, GHC.Base.Monoid view) => GHC.Base.Monad (Ditto.Core.Form m input err view)
+ Ditto.Core: instance forall k (input :: k) (m :: * -> *). GHC.Base.Applicative m => GHC.Base.Applicative (Ditto.Core.NoEnvironment input m)
+ Ditto.Core: instance forall k (input :: k) (m :: * -> *). GHC.Base.Functor m => GHC.Base.Functor (Ditto.Core.NoEnvironment input m)
+ Ditto.Core: instance forall k (input :: k) (m :: * -> *). GHC.Base.Monad m => GHC.Base.Monad (Ditto.Core.NoEnvironment input m)
- Ditto.Core: (@$) :: Monad m => (view -> view') -> Form m input err view a -> Form m input err view' a
+ Ditto.Core: (@$) :: forall (m :: Type -> Type) view view' input err a. Monad m => (view -> view') -> Form m input err view a -> Form m input err view' a
- Ditto.Core: Form :: (input -> m (Either err a)) -> m a -> FormState m (View err view, Result err (Proved a)) -> Form m input err view a
+ Ditto.Core: Form :: (input -> m (Either err a)) -> m a -> FormState m (View err view, Result err (Proved a)) -> Form (m :: Type -> Type) input err view a
- Ditto.Core: NoEnvironment :: m a -> NoEnvironment input m a
+ Ditto.Core: NoEnvironment :: m a -> NoEnvironment (input :: k) (m :: k1 -> Type) (a :: k1)
- Ditto.Core: WithEnvironment :: ReaderT (FormId -> m (Value input)) m a -> WithEnvironment input m a
+ Ditto.Core: WithEnvironment :: ReaderT (FormId -> m (Value input)) m a -> WithEnvironment input (m :: Type -> Type) a
- Ditto.Core: [formDecodeInput] :: Form m input err view a -> input -> m (Either err a)
+ Ditto.Core: [formDecodeInput] :: Form (m :: Type -> Type) input err view a -> input -> m (Either err a)
- Ditto.Core: [formFormlet] :: Form m input err view a -> FormState m (View err view, Result err (Proved a))
+ Ditto.Core: [formFormlet] :: Form (m :: Type -> Type) input err view a -> FormState m (View err view, Result err (Proved a))
- Ditto.Core: [formInitialValue] :: Form m input err view a -> m a
+ Ditto.Core: [formInitialValue] :: Form (m :: Type -> Type) input err view a -> m a
- Ditto.Core: [getNoEnvironment] :: NoEnvironment input m a -> m a
+ Ditto.Core: [getNoEnvironment] :: NoEnvironment (input :: k) (m :: k1 -> Type) (a :: k1) -> m a
- Ditto.Core: [getWithEnvironment] :: WithEnvironment input m a -> ReaderT (FormId -> m (Value input)) m a
+ Ditto.Core: [getWithEnvironment] :: WithEnvironment input (m :: Type -> Type) a -> ReaderT (FormId -> m (Value input)) m a
- Ditto.Core: catchFormError :: Monad m => ([err] -> a) -> Form m input err view a -> Form m input err view a
+ Ditto.Core: catchFormError :: forall (m :: Type -> Type) err a input view. Monad m => ([err] -> a) -> Form m input err view a -> Form m input err view a
- Ditto.Core: catchFormErrorM :: Monad m => Form m input err view a -> ([err] -> Form m input err view a) -> Form m input err view a
+ Ditto.Core: catchFormErrorM :: forall (m :: Type -> Type) input err view a. Monad m => Form m input err view a -> ([err] -> Form m input err view a) -> Form m input err view a
- Ditto.Core: class Monad m => Environment m input | m -> input
+ Ditto.Core: class Monad m => Environment (m :: Type -> Type) input | m -> input
- Ditto.Core: data Form m input err view a
+ Ditto.Core: data Form (m :: Type -> Type) input err view a
- Ditto.Core: getFormId :: Monad m => FormState m FormId
+ Ditto.Core: getFormId :: forall (m :: Type -> Type). Monad m => FormState m FormId
- Ditto.Core: getFormInput :: Environment m input => FormState m (Value input)
+ Ditto.Core: getFormInput :: forall (m :: Type -> Type) input. Environment m input => FormState m (Value input)
- Ditto.Core: getFormInput' :: Environment m input => FormId -> FormState m (Value input)
+ Ditto.Core: getFormInput' :: forall (m :: Type -> Type) input. Environment m input => FormId -> FormState m (Value input)
- Ditto.Core: getFormRange :: Monad m => FormState m FormRange
+ Ditto.Core: getFormRange :: forall (m :: Type -> Type). Monad m => FormState m FormRange
- Ditto.Core: getNamedFormId :: Monad m => Text -> FormState m FormId
+ Ditto.Core: getNamedFormId :: forall (m :: Type -> Type). Monad m => Text -> FormState m FormId
- Ditto.Core: mapResult :: Monad m => (Result err (Proved a) -> Result err (Proved a)) -> (View err view -> View err view) -> Form m input err view a -> Form m input err view a
+ Ditto.Core: mapResult :: forall (m :: Type -> Type) err a view input. Monad m => (Result err (Proved a) -> Result err (Proved a)) -> (View err view -> View err view) -> Form m input err view a -> Form m input err view a
- Ditto.Core: mapView :: Functor m => (view -> view') -> Form m input err view a -> Form m input err view' a
+ Ditto.Core: mapView :: forall (m :: Type -> Type) view view' input err a. Functor m => (view -> view') -> Form m input err view a -> Form m input err view' a
- Ditto.Core: mkOk :: Monad m => FormId -> view -> a -> FormState m (View err view, Result err (Proved a))
+ Ditto.Core: mkOk :: forall (m :: Type -> Type) view a err. Monad m => FormId -> view -> a -> FormState m (View err view, Result err (Proved a))
- Ditto.Core: newtype NoEnvironment input m a
+ Ditto.Core: newtype NoEnvironment (input :: k) (m :: k1 -> Type) (a :: k1)
- Ditto.Core: newtype WithEnvironment input m a
+ Ditto.Core: newtype WithEnvironment input (m :: Type -> Type) a
- Ditto.Core: pureRes :: (Monad m, Monoid view, FormError input err) => a -> Either err a -> Form m input err view a
+ Ditto.Core: pureRes :: forall (m :: Type -> Type) view a err input. (Monad m, Monoid view) => a -> Either err a -> Form m input err view a
- Ditto.Core: type FormState m = StateT FormRange m
+ Ditto.Core: type FormState (m :: Type -> Type) = StateT FormRange m
- Ditto.Core: view :: Monad m => view -> Form m input err view ()
+ Ditto.Core: view :: forall (m :: Type -> Type) view input err. Monad m => view -> Form m input err view ()
- Ditto.Generalized.Named: childErrors :: Environment m input => ([err] -> view) -> Form m input err view ()
+ Ditto.Generalized.Named: childErrors :: forall (m :: Type -> Type) input err view. Environment m input => ([err] -> view) -> Form m input err view ()
- Ditto.Generalized.Named: errors :: Environment m input => ([err] -> view) -> Form m input err view ()
+ Ditto.Generalized.Named: errors :: forall (m :: Type -> Type) input err view. Environment m input => ([err] -> view) -> Form m input err view ()
- Ditto.Generalized.Named: input :: (Environment m input, FormError input err) => Text -> (input -> Either err a) -> (FormId -> a -> view) -> a -> Form m input err view a
+ Ditto.Generalized.Named: input :: forall (m :: Type -> Type) input err a view. (Environment m input, FormError input err) => Text -> (input -> Either err a) -> (FormId -> a -> view) -> a -> Form m input err view a
- Ditto.Generalized.Named: inputChoice :: forall a m err input lbl view. (FormError input err, FormInput input, Environment m input, Eq a, Monoid view) => Text -> (a -> Bool) -> NonEmpty (a, lbl) -> (input -> Either err a) -> (FormId -> [Choice lbl a] -> view) -> Form m input err view a
+ Ditto.Generalized.Named: inputChoice :: forall a (m :: Type -> Type) err input lbl view. (FormError input err, Environment m input, Eq a) => Text -> (a -> Bool) -> NonEmpty (a, lbl) -> (input -> Either err a) -> (FormId -> [Choice lbl a] -> view) -> Form m input err view a
- Ditto.Generalized.Named: inputFile :: forall m input err view ft. (Environment m input, FormInput input, FormError input err, ft ~ FileType input, Monoid ft) => Text -> (FormId -> view) -> Form m input err view (FileType input)
+ Ditto.Generalized.Named: inputFile :: forall (m :: Type -> Type) input err view ft. (Environment m input, FormInput input, FormError input err, ft ~ FileType input, Monoid ft) => Text -> (FormId -> view) -> Form m input err view (FileType input)
- Ditto.Generalized.Named: inputList :: forall m input err a view. (Monad m, FormError input err, Environment m input) => Text -> (input -> m (Either err [a])) -> ([view] -> view) -> [a] -> view -> (a -> Form m input err view a) -> Form m input err view [a]
+ Ditto.Generalized.Named: inputList :: Environment m input => Text -> (input -> m (Either err [a])) -> ([view] -> view) -> [a] -> view -> (a -> Form m input err view a) -> Form m input err view [a]
- Ditto.Generalized.Named: inputMaybe :: (Environment m input, FormError input err) => Text -> (input -> Either err a) -> (FormId -> Maybe a -> view) -> Maybe a -> Form m input err view (Maybe a)
+ Ditto.Generalized.Named: inputMaybe :: forall (m :: Type -> Type) input err a view. Environment m input => Text -> (input -> Either err a) -> (FormId -> Maybe a -> view) -> Maybe a -> Form m input err view (Maybe a)
- Ditto.Generalized.Named: inputMulti :: forall m input err view a lbl. (FormError input err, FormInput input, Environment m input, Eq a) => Text -> [(a, lbl)] -> (input -> Either err [a]) -> (FormId -> [Choice lbl a] -> view) -> (a -> Bool) -> Form m input err view [a]
+ Ditto.Generalized.Named: inputMulti :: forall (m :: Type -> Type) input err view a lbl. (Environment m input, Eq a) => Text -> [(a, lbl)] -> (input -> Either err [a]) -> (FormId -> [Choice lbl a] -> view) -> (a -> Bool) -> Form m input err view [a]
- Ditto.Generalized.Named: inputNoData :: Environment m input => Text -> (FormId -> view) -> Form m input err view ()
+ Ditto.Generalized.Named: inputNoData :: forall (m :: Type -> Type) input view err. Environment m input => Text -> (FormId -> view) -> Form m input err view ()
- Ditto.Generalized.Named: iopt :: forall m input view err a. (Monoid view, Environment m input, FormError input err) => Text -> (input -> Either err a) -> Maybe a -> Form m input err view (Maybe a)
+ Ditto.Generalized.Named: iopt :: forall (m :: Type -> Type) input view err a. (Monoid view, Environment m input) => Text -> (input -> Either err a) -> Maybe a -> Form m input err view (Maybe a)
- Ditto.Generalized.Named: ireq :: forall m input view err a. (Monoid view, Environment m input, FormError input err) => Text -> (input -> Either err a) -> a -> Form m input err view a
+ Ditto.Generalized.Named: ireq :: forall (m :: Type -> Type) input view err a. (Monoid view, Environment m input, FormError input err) => Text -> (input -> Either err a) -> a -> Form m input err view a
- Ditto.Generalized.Named: label :: Environment m input => Text -> (FormId -> view) -> Form m input err view ()
+ Ditto.Generalized.Named: label :: forall (m :: Type -> Type) input view err. Environment m input => Text -> (FormId -> view) -> Form m input err view ()
- Ditto.Generalized.Named: withChildErrors :: Monad m => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
+ Ditto.Generalized.Named: withChildErrors :: forall (m :: Type -> Type) view err input a. Monad m => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
- Ditto.Generalized.Named: withErrors :: Environment m input => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
+ Ditto.Generalized.Named: withErrors :: forall (m :: Type -> Type) input view err a. Environment m input => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
- Ditto.Generalized.Unnamed: childErrors :: Environment m input => ([err] -> view) -> Form m input err view ()
+ Ditto.Generalized.Unnamed: childErrors :: forall (m :: Type -> Type) input err view. Environment m input => ([err] -> view) -> Form m input err view ()
- Ditto.Generalized.Unnamed: errors :: Environment m input => ([err] -> view) -> Form m input err view ()
+ Ditto.Generalized.Unnamed: errors :: forall (m :: Type -> Type) input err view. Environment m input => ([err] -> view) -> Form m input err view ()
- Ditto.Generalized.Unnamed: input :: (Environment m input, FormError input err) => (input -> Either err a) -> (FormId -> a -> view) -> a -> Form m input err view a
+ Ditto.Generalized.Unnamed: input :: forall (m :: Type -> Type) input err a view. (Environment m input, FormError input err) => (input -> Either err a) -> (FormId -> a -> view) -> a -> Form m input err view a
- Ditto.Generalized.Unnamed: inputChoice :: forall a m err input lbl view. (FormError input err, FormInput input, Environment m input, Eq a, Monoid view) => (a -> Bool) -> NonEmpty (a, lbl) -> (input -> Either err a) -> (FormId -> [Choice lbl a] -> view) -> Form m input err view a
+ Ditto.Generalized.Unnamed: inputChoice :: forall a (m :: Type -> Type) err input lbl view. (FormError input err, Environment m input, Eq a) => (a -> Bool) -> NonEmpty (a, lbl) -> (input -> Either err a) -> (FormId -> [Choice lbl a] -> view) -> Form m input err view a
- Ditto.Generalized.Unnamed: inputFile :: forall m input err view ft. (Environment m input, FormInput input, FormError input err, ft ~ FileType input, Monoid ft) => (FormId -> view) -> Form m input err view (FileType input)
+ Ditto.Generalized.Unnamed: inputFile :: forall (m :: Type -> Type) input err view ft. (Environment m input, FormInput input, FormError input err, ft ~ FileType input, Monoid ft) => (FormId -> view) -> Form m input err view (FileType input)
- Ditto.Generalized.Unnamed: inputList :: forall m input err a view. (Monad m, FormError input err, Environment m input) => (input -> m (Either err [a])) -> ([view] -> view) -> [a] -> view -> (a -> Form m input err view a) -> Form m input err view [a]
+ Ditto.Generalized.Unnamed: inputList :: Environment m input => (input -> m (Either err [a])) -> ([view] -> view) -> [a] -> view -> (a -> Form m input err view a) -> Form m input err view [a]
- Ditto.Generalized.Unnamed: inputMaybe :: (Environment m input, FormError input err) => (input -> Either err a) -> (FormId -> Maybe a -> view) -> Maybe a -> Form m input err view (Maybe a)
+ Ditto.Generalized.Unnamed: inputMaybe :: forall (m :: Type -> Type) input err a view. Environment m input => (input -> Either err a) -> (FormId -> Maybe a -> view) -> Maybe a -> Form m input err view (Maybe a)
- Ditto.Generalized.Unnamed: inputMulti :: forall m input err view a lbl. (FormError input err, FormInput input, Environment m input, Eq a) => [(a, lbl)] -> (input -> Either err [a]) -> (FormId -> [Choice lbl a] -> view) -> (a -> Bool) -> Form m input err view [a]
+ Ditto.Generalized.Unnamed: inputMulti :: forall (m :: Type -> Type) input err view a lbl. (Environment m input, Eq a) => [(a, lbl)] -> (input -> Either err [a]) -> (FormId -> [Choice lbl a] -> view) -> (a -> Bool) -> Form m input err view [a]
- Ditto.Generalized.Unnamed: inputNoData :: Environment m input => (FormId -> view) -> Form m input err view ()
+ Ditto.Generalized.Unnamed: inputNoData :: forall (m :: Type -> Type) input view err. Environment m input => (FormId -> view) -> Form m input err view ()
- Ditto.Generalized.Unnamed: label :: Environment m input => (FormId -> view) -> Form m input err view ()
+ Ditto.Generalized.Unnamed: label :: forall (m :: Type -> Type) input view err. Environment m input => (FormId -> view) -> Form m input err view ()
- Ditto.Generalized.Unnamed: withChildErrors :: Monad m => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
+ Ditto.Generalized.Unnamed: withChildErrors :: forall (m :: Type -> Type) view err input a. Monad m => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
- Ditto.Generalized.Unnamed: withErrors :: Environment m input => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
+ Ditto.Generalized.Unnamed: withErrors :: forall (m :: Type -> Type) input view err a. Environment m input => (view -> [err] -> view) -> Form m input err view a -> Form m input err view a
- Ditto.Proof: Proof :: (a -> m (Either err b)) -> (a -> b) -> Proof m err a b
+ Ditto.Proof: Proof :: (a -> m (Either err b)) -> (a -> b) -> Proof (m :: Type -> Type) err a b
- Ditto.Proof: [proofFunction] :: Proof m err a b -> a -> m (Either err b)
+ Ditto.Proof: [proofFunction] :: Proof (m :: Type -> Type) err a b -> a -> m (Either err b)
- Ditto.Proof: [proofNewInitialValue] :: Proof m err a b -> a -> b
+ Ditto.Proof: [proofNewInitialValue] :: Proof (m :: Type -> Type) err a b -> a -> b
- Ditto.Proof: data Proof m err a b
+ Ditto.Proof: data Proof (m :: Type -> Type) err a b
- Ditto.Proof: decimal :: (Monad m, Eq i, Num i) => (String -> error) -> i -> Proof m error String i
+ Ditto.Proof: decimal :: forall (m :: Type -> Type) i error. (Monad m, Eq i, Num i) => (String -> error) -> i -> Proof m error String i
- Ditto.Proof: notNullProof :: Monad m => error -> Proof m error [a] [a]
+ Ditto.Proof: notNullProof :: forall (m :: Type -> Type) error a. Monad m => error -> Proof m error [a] [a]
- Ditto.Proof: prove :: (Monad m, Monoid view, FormError input error) => Form m input error view a -> Proof m error a b -> Form m input error view b
+ Ditto.Proof: prove :: forall (m :: Type -> Type) input error view a b. Monad m => Form m input error view a -> Proof m error a b -> Form m input error view b
- Ditto.Proof: realFrac :: (Monad m, RealFrac a) => (String -> error) -> a -> Proof m error String a
+ Ditto.Proof: realFrac :: forall (m :: Type -> Type) a error. (Monad m, RealFrac a) => (String -> error) -> a -> Proof m error String a
- Ditto.Proof: realFracSigned :: (Monad m, RealFrac a) => (String -> error) -> a -> Proof m error String a
+ Ditto.Proof: realFracSigned :: forall (m :: Type -> Type) a error. (Monad m, RealFrac a) => (String -> error) -> a -> Proof m error String a
- Ditto.Proof: signedDecimal :: (Monad m, Eq i, Real i) => (String -> error) -> i -> Proof m error String i
+ Ditto.Proof: signedDecimal :: forall (m :: Type -> Type) i error. (Monad m, Real i) => (String -> error) -> i -> Proof m error String i
- Ditto.Proof: transformEither :: (Monad m, Monoid view, FormError input error) => Form m input error view a -> (a -> Either error b) -> (a -> b) -> Form m input error view b
+ Ditto.Proof: transformEither :: forall (m :: Type -> Type) input error view a b. Monad m => Form m input error view a -> (a -> Either error b) -> (a -> b) -> Form m input error view b
- Ditto.Proof: transformEitherM :: (Monad m, Monoid view, FormError input error) => Form m input error view a -> (a -> m (Either error b)) -> (a -> b) -> Form m input error view b
+ Ditto.Proof: transformEitherM :: Monad m => Form m input error view a -> (a -> m (Either error b)) -> (a -> b) -> Form m input error view b
- Ditto.Types: pattern Error :: forall e ok. [(FormRange, e)] -> Result e ok
+ Ditto.Types: pattern Error :: [(FormRange, e)] -> Result e ok

Files

+ CHANGELOG.md view
@@ -0,0 +1,25 @@+# Changelog++## 0.5.0 — 2026-08-27++### Changed+- Prefer GHC 9.10+ (`GHC2024`) with automatic fallback to `GHC2021` / `Haskell2010` on older GHC+- Widen dependency bounds (`base >= 4.14`, `text >= 1.2`) and drop the unused `containers` dependency+- Enable a stricter warning set on GHC 8.10+ (`-Wcompat`, unused packages, missing deriving strategies, …)+- Replace per-module `LANGUAGE` pragmas with cabal `default-extensions`+- Drop redundant constraints from public signatures (`prove`, `inputList`, `inputMaybe`, …); `inputMulti` does not require `FormInput` because decoding is caller-supplied+- Fix `inputFile` so `Default` environment renders an empty upload instead of an error (`Missing` on submit is unchanged)++### Added+- Export `hoistForm`+- Test suite covering decode, `Alternative`, `catchFormError`, `inputMulti`, `inputChoice`, and `inputFile` (including `Missing` submit path)+- GitHub Actions CI: Cabal matrix (GHC 9.6–9.14) and `nix flake check`+- `cabal.project`+- Nix flake (`flake.nix`, `flake.lock`) with `packages`, `checks`, and `devShells` outputs++### Removed+- `Setup.hs` (unused with `build-type: Simple`)++### Packaging+- Nix is flake-only; legacy `default.nix` and `shell.nix` removed+- nixpkgs pinned by rev in `flake.nix` / `flake.lock`
+ README.md view
@@ -0,0 +1,42 @@+ditto [![Hackage](https://img.shields.io/hackage/v/ditto.svg)](https://hackage.haskell.org/package/ditto)+=========++![Image of ditto from pokemon](https://i.imgur.com/0i7qeeW.png)+> Its transformation ability is perfect. However, if it is made to laugh, it can't maintain its disguise.++A portable library which provides type-safe form generation and validation.++This core library is intended to be used in conjunction with other libraries such as [scotty-form](http://hackage.haskell.org/package/scotty-form) and [ditto-lucid](http://hackage.haskell.org/package/ditto-lucid).++## Requirements++Tested on GHC 9.6 through 9.14. GHC 9.10+ uses `GHC2024` by default; older supported GHC versions fall back to `GHC2021` or `Haskell2010` with the required extensions enabled in the cabal file.++### Cabal++Use this for day-to-day development (fast incremental rebuilds):++```+cabal test --enable-tests+```++### Nix (flake)++Pinned nixpkgs and package checks:++```+nix build+nix flake check+nix develop+```++- `nix flake check` — build library and run the test suite in an isolated Nix derivation (CI uses this).+- `nix develop` — enter a shell with GHC, `cabal-install`, and HLS; run `cabal test` for iterative work.+- Flake source includes all project files under the repo root (not only git-tracked paths). `flake.lock` pins nixpkgs.++## 0.5.0 upgrade notes++- Recommended toolchain: GHC 9.10+ for `GHC2024`.+- Dependency bounds are intentionally permissive (`base >= 4.14`); older GHC releases remain supported where possible.+- `inputFile`: `Default` environment is an empty upload (no error on initial render); `Missing` on submit still errors.+- `hoistForm` is exported from `Ditto.Core`.
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
ditto.cabal view
@@ -1,6 +1,6 @@-cabal-version: 3.0+cabal-version: 3.8 name: ditto-version: 0.4.1+version: 0.5.0 synopsis: ditto is a type-safe HTML form generation and validation library description:   ditto follows in the footsteps of formlets and@@ -14,15 +14,74 @@ copyright:   2012 Jeremy Shaw, Jasper Van der Jeugt, SeeReason Partners LLC,   2019 Zachary Churchill+homepage: https://github.com/goolord/ditto+bug-reports: https://github.com/goolord/ditto/issues category: Web build-type: Simple+extra-doc-files:+  CHANGELOG.md+  README.md+tested-with:+  GHC == 9.6.7+  GHC == 9.8.4+  GHC == 9.10.3+  GHC == 9.12.4+  GHC == 9.14.1  source-repository head   type: git   location: https://github.com/goolord/ditto.git -library+common warnings   ghc-options: -Wall+  if impl(ghc >= 8.10)+  ghc-options:+    -Wcompat+    -Widentities+    -Wincomplete-record-updates+    -Wincomplete-uni-patterns+    -Wmissing-deriving-strategies+    -Wunused-packages+  if impl(ghc >= 9.4)+  ghc-options: -Wredundant-constraints++common extensions+  default-extensions:+    BangPatterns+    DataKinds+    DeriveFoldable+    DeriveFunctor+    DeriveTraversable+    DerivingStrategies+    ExplicitForAll+    FlexibleInstances+    FunctionalDependencies+    GeneralizedNewtypeDeriving+    LambdaCase+    MultiParamTypeClasses+    NamedFieldPuns+    OverloadedStrings+    PatternSynonyms+    RankNTypes+    RecordWildCards+    ScopedTypeVariables+    StandaloneDeriving+    TypeApplications+    TypeFamilies+    TypeOperators++common defaults+  import: warnings, extensions+  if impl(ghc >= 9.10)+    default-language: GHC2024+  else+    if impl(ghc >= 9.2)+      default-language: GHC2021+    else+      default-language: Haskell2010++library+  import: defaults   exposed-modules:     Ditto     Ditto.Core@@ -34,9 +93,18 @@   other-modules:     Ditto.Generalized.Internal   build-depends:-      base       >= 4    && < 5-    , containers >= 0.4  && < 1.0-    , mtl        >= 2.0  && < 3.0-    , text       >= 0.11 && < 3.0+      base >= 4.14 && < 5+    , mtl  >= 2.2  && < 3+    , text >= 1.2  && < 3   hs-source-dirs: src-  default-language: Haskell2010++test-suite ditto-test+  import: defaults+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs: test+  build-depends:+      base+    , ditto+    , mtl+    , text
src/Ditto/Backend.hs view
@@ -1,10 +1,3 @@-{-# LANGUAGE-    MultiParamTypeClasses-  , TypeFamilies-  , OverloadedStrings-  , FunctionalDependencies-#-}- {- | This module contains two classes. 'FormInput' is a class which is parameterized over the @input@ type used to represent form data in different web frameworks. There should be one instance for each framework, such as Happstack, Snap, WAI, etc. @@ -29,7 +22,7 @@   | MultiFilesFound input   | MultiStringsFound input   | MissingDefaultValue-  deriving (Eq, Ord, Show)+  deriving stock (Eq, Ord, Show)  -- | some default error messages for 'CommonFormError' commonFormErrorStr
src/Ditto/Core.hs view
@@ -1,18 +1,3 @@-{-# LANGUAGE-    DeriveFunctor-  , FlexibleInstances-  , FunctionalDependencies-  , GeneralizedNewtypeDeriving-  , LambdaCase-  , NamedFieldPuns-  , OverloadedStrings-  , RankNTypes-  , RecordWildCards-  , ScopedTypeVariables-  , StandaloneDeriving-  , TypeApplications-#-}- -- | The core module for @ditto@. -- -- This module provides the @Form@ type and helper functions@@ -53,6 +38,7 @@   , unitRange   , view   , viewForm+  , hoistForm   , pureRes   , liftForm   ) where@@ -79,7 +65,7 @@   { formDecodeInput :: input -> m (Either err a) -- ^ Decode the value from the input   , formInitialValue :: m a -- ^ The initial value   , formFormlet :: FormState m (View err view, Result err (Proved a)) -- ^ A @FormState@ which produces a @View@ and a @Result@-  } deriving (Functor)+  } deriving stock (Functor)  instance (Monad m, Monoid view) => Applicative (Form m input err view) where @@ -122,7 +108,7 @@     (v2, _) <- formFormlet f2     pure (v1 <> v2, r) -instance (Environment m input, Monoid view, FormError input err) => Monad (Form m input err view) where+instance (Environment m input, Monoid view) => Monad (Form m input err view) where   form >>= f =     let mres = snd <$> runForm "" form     in Form@@ -189,7 +175,7 @@  -- | Run the form, but always return the initial value newtype NoEnvironment input m a = NoEnvironment { getNoEnvironment ::  m a }-  deriving (Monad, Functor, Applicative)+  deriving newtype (Functor, Applicative, Monad)  instance Monad m => Environment (NoEnvironment input m) input where   environment = noEnvironment@@ -200,9 +186,9 @@  -- | Run the form, but with a given @environment@ function newtype WithEnvironment input m a = WithEnvironment { getWithEnvironment :: ReaderT (FormId -> m (Value input)) m a }-  deriving (Monad, Functor, Applicative)+  deriving newtype (Functor, Applicative, Monad) -deriving instance Monad m => MonadReader (FormId -> m (Value input)) (WithEnvironment input m)+deriving newtype instance Monad m => MonadReader (FormId -> m (Value input)) (WithEnvironment input m)  instance MonadTrans (WithEnvironment input) where   lift = WithEnvironment . lift@@ -416,8 +402,8 @@  -- | Lift a monad morphism from @m@ to @n@ into a monad morphism from @(Form m)@ to @(Form n)@ -- eg. @newtype@s, @lift@s-hoistForm :: (Monad f)-  => (forall x. m x -> f x)+hoistForm+  :: (forall x. m x -> f x)   -> Form m input err view a   -> Form f input err view a hoistForm f Form{formDecodeInput, formInitialValue, formFormlet} = Form@@ -470,7 +456,7 @@   pure (unView v [])  -- | lift the result of a decoding to a @Form@-pureRes :: (Monad m, Monoid view, FormError input err)+pureRes :: (Monad m, Monoid view)   => a   -> Either err a   -> Form m input err view a
src/Ditto/Generalized/Internal.hs view
@@ -1,10 +1,3 @@-{-# LANGUAGE-    NamedFieldPuns-  , ScopedTypeVariables-  , LambdaCase-  , TypeFamilies-#-}- -- | This module provides helper functions for HTML input elements. These helper functions are not specific to any particular web framework or html library.  module Ditto.Generalized.Internal where@@ -56,7 +49,7 @@         )  -- | this is necessary in order to basically map over the decoding function-inputList :: forall m input err a view view'. (Monad m, FormError input err, Environment m input)+inputList :: forall m input err a view view'. Environment m input   => FormState m FormId   -> (input -> m (Either err [a])) -- ^ decoding function for the list   -> ([view] -> view') -- ^ how to concatenate views@@ -111,7 +104,7 @@           )  -- | used for elements like @\<input type=\"submit\"\>@ which are not always present in the form submission data.-inputMaybe :: (Monad m, FormError input err, Environment m input)+inputMaybe :: Environment m input   => FormState m FormId   -> (input -> Either err a)   -> (FormId -> Maybe a -> view)@@ -166,19 +159,22 @@       )  -- | used for @\<input type=\"file\"\>@-inputFile :: forall m ft input err view. (Monad m, FormInput input, FormError input err, Environment m input, ft ~ FileType input, Monoid ft)+inputFile :: forall m ft input err view. (FormInput input, FormError input err, Environment m input, ft ~ FileType input, Monoid ft)   => FormState m FormId   -> (FormId -> view)   -> Form m input err view (FileType input) inputFile i' toView =-  Form (pure . getInputFile) (pure mempty) $ do -- FIXME+  Form (pure . getInputFile) (pure mempty) $ do     i <- i'     v <- getFormInput' i     case v of       Default ->         pure           ( View $ const $ toView i-          , Error [(unitRange i, commonFormError (InputMissing i :: CommonFormError input) :: err)]+          , Ok Proved+              { pos = unitRange i+              , unProved = mempty+              }           )       Found x -> case getInputFile x of         Right a -> pure@@ -199,7 +195,7 @@           )  -- | used for groups of checkboxes, @\<select multiple=\"multiple\"\>@ boxes-inputMulti :: forall m input err view a lbl. (FormError input err, FormInput input, Environment m input, Eq a)+inputMulti :: forall m input err view a lbl. (Environment m input, Eq a)   => FormState m FormId   -> [(a, lbl)] -- ^ value, label, initially checked   -> (input -> Either err [a])@@ -257,7 +253,7 @@   }  -- | radio buttons, single @\<select\>@ boxes-inputChoice :: forall a m err input lbl view. (FormError input err, FormInput input, Monad m, Eq a, Monoid view, Environment m input)+inputChoice :: forall a m err input lbl view. (FormError input err, Eq a, Environment m input)   => FormState m FormId   -> (a -> Bool) -- ^ is default   -> NonEmpty (a, lbl) -- ^ value, label
src/Ditto/Generalized/Named.hs view
@@ -1,8 +1,3 @@-{-# LANGUAGE -    ScopedTypeVariables-  , TypeFamilies-#-}- -- | This module provides helper functions for HTML input elements. These helper functions are not specific to any particular web framework or html library. -- -- For unnamed (enumerated) formlets, see @Ditto.Generalized.Unnamed@@@ -42,7 +37,7 @@ input name = G.input (getNamedFormId name)  -- | used for elements like @\<input type=\"submit\"\>@ which are not always present in the form submission data.-inputMaybe :: (Environment m input, FormError input err)+inputMaybe :: Environment m input   => Text   -> (input -> Either err a)   -> (FormId -> Maybe a -> view)@@ -65,7 +60,7 @@ inputFile name = G.inputFile (getNamedFormId name)  -- | used for groups of checkboxes, @\<select multiple=\"multiple\"\>@ boxes-inputMulti :: forall m input err view a lbl. (FormError input err, FormInput input, Environment m input, Eq a)+inputMulti :: forall m input err view a lbl. (Environment m input, Eq a)   => Text   -> [(a, lbl)] -- ^ value, label, initially checked   -> (input -> Either err [a])@@ -75,7 +70,7 @@ inputMulti name = G.inputMulti (getNamedFormId name)  -- | radio buttons, single @\<select\>@ boxes-inputChoice :: forall a m err input lbl view. (FormError input err, FormInput input, Environment m input, Eq a, Monoid view)+inputChoice :: forall a m err input lbl view. (FormError input err, Environment m input, Eq a)   => Text   -> (a -> Bool) -- ^ is default   -> NonEmpty (a, lbl) -- ^ value, label@@ -85,7 +80,7 @@ inputChoice name = G.inputChoice (getNamedFormId name)  -- | this is necessary in order to basically map over the decoding function-inputList :: forall m input err a view. (Monad m, FormError input err, Environment m input)+inputList :: forall m input err a view. Environment m input   => Text   -> (input -> m (Either err [a])) -- ^ decoding function for the list   -> ([view] -> view) -- ^ how to concatenate views@@ -160,7 +155,7 @@       )  -- | an optional @Form@ with no @view@-iopt :: forall m input view err a. (Monoid view, Environment m input, FormError input err)+iopt :: forall m input view err a. (Monoid view, Environment m input)   => Text    -> (input -> Either err a)   -> Maybe a
src/Ditto/Generalized/Unnamed.hs view
@@ -1,8 +1,3 @@-{-# LANGUAGE -    ScopedTypeVariables-  , TypeFamilies-#-}- -- | This module provides helper functions for HTML input elements. These helper functions are not specific to any particular web framework or html library. -- -- Additionally, the inputs generated with the functions from this module will have their names/ids automatically enumerated.@@ -39,7 +34,7 @@ input = G.input getFormId  -- | used for elements like @\<input type=\"submit\"\>@ which are not always present in the form submission data.-inputMaybe :: (Environment m input, FormError input err)+inputMaybe :: Environment m input   => (input -> Either err a)   -> (FormId -> Maybe a -> view)   -> Maybe a@@ -59,7 +54,7 @@ inputFile = G.inputFile getFormId  -- | used for groups of checkboxes, @\<select multiple=\"multiple\"\>@ boxes-inputMulti :: forall m input err view a lbl. (FormError input err, FormInput input, Environment m input, Eq a)+inputMulti :: forall m input err view a lbl. (Environment m input, Eq a)   => [(a, lbl)] -- ^ value, label, initially checked   -> (input -> Either err [a])   -> (FormId -> [G.Choice lbl a] -> view) -- ^ function which generates the view@@ -68,7 +63,7 @@ inputMulti = G.inputMulti getFormId  -- | radio buttons, single @\<select\>@ boxes-inputChoice :: forall a m err input lbl view. (FormError input err, FormInput input, Environment m input, Eq a, Monoid view)+inputChoice :: forall a m err input lbl view. (FormError input err, Environment m input, Eq a)   => (a -> Bool) -- ^ is default   -> NonEmpty (a, lbl) -- ^ value, label   -> (input -> Either err a)@@ -77,7 +72,7 @@ inputChoice = G.inputChoice getFormId  -- | this is necessary in order to basically map over the decoding function-inputList :: forall m input err a view. (Monad m, FormError input err, Environment m input)+inputList :: forall m input err a view. Environment m input   => (input -> m (Either err [a])) -- ^ decoding function for the list   -> ([view] -> view) -- ^ how to concatenate views   -> [a] -- ^ initial values
src/Ditto/Proof.hs view
@@ -1,9 +1,3 @@-{-# LANGUAGE-    DeriveFunctor-  , NamedFieldPuns-  , ScopedTypeVariables-#-}- {- | This module defines the 'Proof' type, some proofs, and some helper functions. @@ -18,7 +12,6 @@  import Control.Monad.Trans (lift) import Ditto.Core (Form(..))-import Ditto.Backend (FormError(..)) import Ditto.Types (Proved(..), Result(..)) import Numeric (readDec, readFloat, readSigned) @@ -34,11 +27,11 @@ data Proof m err a b = Proof   { proofFunction :: a -> m (Either err b) -- ^ function which provides the proof   , proofNewInitialValue :: a -> b -- ^ usually @const b@-  } deriving (Functor)+  } deriving stock (Functor)  -- | apply a 'Proof' to a 'Form' prove-  :: (Monad m, Monoid view, FormError input error)+  :: Monad m   => Form m input error view a   -> Proof m error a b   -> Form m input error view b@@ -69,7 +62,7 @@  -- | transform the 'Form' result using a monadic 'Either' function. transformEitherM-  :: (Monad m, Monoid view, FormError input error)+  :: Monad m   => Form m input error view a   -> (a -> m (Either error b))   -> (a -> b)@@ -78,7 +71,7 @@  -- | transform the 'Form' result using an 'Either' function. transformEither-  :: (Monad m, Monoid view, FormError input error)+  :: Monad m   => Form m input error view a   -> (a -> Either error b)   -> (a -> b)@@ -110,7 +103,7 @@         _ -> Left $ mkError str  -- | read signed decimal number-signedDecimal :: (Monad m, Eq i, Real i)+signedDecimal :: (Monad m, Real i)   => (String -> error)   -> i   -> Proof m error String i
src/Ditto/Types.hs view
@@ -1,15 +1,3 @@-{-# LANGUAGE-    BangPatterns-  , DeriveFoldable-  , DeriveFunctor-  , DeriveTraversable-  , GeneralizedNewtypeDeriving-  , MultiParamTypeClasses-  , OverloadedStrings-  , PatternSynonyms-  , ExplicitForAll-#-}- -- | Types relevant to forms and their validation. module Ditto.Types (   -- * FormId@@ -43,7 +31,7 @@   | FormIdName       {-# UNPACK #-} !Text -- ^ Local name of the input       {-# UNPACK #-} !Int  -- ^ Index of the input-  deriving (Eq, Ord, Show)+  deriving stock (Eq, Ord, Show)  instance IsString FormId where   fromString x = FormIdName (T.pack x) 0@@ -64,7 +52,7 @@ -- | A range of ID's to specify a group of forms data FormRange   = FormRange FormId FormId-  deriving (Eq, Show)+  deriving stock (Eq, Show)  ------------------------------------------------------------------------------ -- Form result types@@ -74,7 +62,7 @@  -- | Function which creates the form view newtype View err v = View { unView :: [(FormRange, err)] -> v }-  deriving (Semigroup, Monoid, Functor)+  deriving newtype (Semigroup, Monoid, Functor)  -- | used to represent whether a value was found in the form -- submission data, missing from the form submission data, or expected@@ -83,7 +71,7 @@   = Default   | Missing   | Found a-  deriving (Eq, Show, Functor, Traversable, Foldable)+  deriving stock (Eq, Show, Functor, Foldable, Traversable)  instance Applicative Value where   pure = Found@@ -111,7 +99,8 @@ -- | Type for failing computations -- Similar to @Either@ but with an accumilating @Applicative@ instance newtype Result e ok = Result { getResult :: Either [(FormRange, e)] ok }-  deriving (Show, Eq, Functor, Foldable, Traversable, Monad)+  deriving newtype (Eq, Show, Functor, Foldable, Monad)+  deriving stock (Traversable)  pattern Error :: forall e ok. [(FormRange, e)] -> Result e ok pattern Error e = Result (Left e)@@ -130,4 +119,4 @@ data Proved a = Proved   { pos :: FormRange   , unProved :: a-  } deriving (Show, Functor, Foldable, Traversable)+  } deriving stock (Show, Functor, Foldable, Traversable)
+ test/Main.hs view
@@ -0,0 +1,294 @@+module Main (main) where++import Control.Applicative ((<|>))+import Control.Monad (unless)+import Control.Monad.Reader (runReaderT)+import Data.Functor.Identity (Identity, runIdentity)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Text (Text)+import Ditto+import Ditto.Generalized.Named qualified as Named+import Ditto.Generalized.Unnamed qualified as Unnamed+import System.Exit (exitFailure)+import System.IO (hPutStrLn, stderr)+import Text.Read (readMaybe)++import Data.Text qualified as T++-- | Minimal backend input type for file-upload tests.+newtype TestInput = TestInput Text+  deriving stock (Show)+  deriving newtype (Eq)++instance FormInput TestInput where+  type FileType TestInput = Text+  getInputTexts (TestInput t) = [t]+  getInputFile (TestInput t) = Right t++instance FormError TestInput Text where+  commonFormError = commonFormErrorText (T.pack . show)++main :: IO ()+main = do+  testEncodeFormId+  testResultApplicative+  testValueInstances+  testViewForm+  testNamedInput+  testUnnamedInput+  testIreqIopt+  testProofs+  testProofDecode+  testHoistForm+  testAlternative+  testCatchFormError+  testInputMulti+  testInputChoice+  testInputFile+  putStrLn "OK"++assertEq :: (Eq a, Show a) => String -> a -> a -> IO ()+assertEq label expected actual =+  unless (expected == actual) $ do+    hPutStrLn stderr $ "FAIL: " <> label+    hPutStrLn stderr $ "  expected: " <> show expected+    hPutStrLn stderr $ "  got:      " <> show actual+    exitFailure++testEncodeFormId :: IO ()+testEncodeFormId = do+  assertEq "encodeFormId numbered"+    "user-val-0.1.2"+    (encodeFormId (FormId "user" (0 :| [1, 2])))+  assertEq "encodeFormId named"+    "email"+    (encodeFormId (FormIdName "email" 3))+  assertEq "formIdentifier numbered" 0 (formIdentifier (FormId "user" (0 :| [1])))+  assertEq "formIdentifier named" 3 (formIdentifier (FormIdName "email" 3))++range0 :: FormRange+range0 = FormRange (FormIdName "a" 0) (FormIdName "a" 1)++testResultApplicative :: IO ()+testResultApplicative = do+  assertEq "Result accumulates errors"+    (Error [(range0, "a" :: Text), (range0, "b")] :: Result Text Int)+    (Error [(range0, "a")] <*> Error [(range0, "b")])+  assertEq "Result Ok <*> Ok"+    (Ok (3 :: Int) :: Result Text Int)+    (Ok (+ 1) <*> Ok (2 :: Int))++testValueInstances :: IO ()+testValueInstances = do+  assertEq "Found <*> Found" (Found (3 :: Int)) (Found (+ 1) <*> Found (2 :: Int))+  assertEq "Missing <|> Found" (Found (1 :: Int)) (Missing <|> Found 1)+  assertEq "Found <> Found" (Found ("ab" :: Text)) (Found "a" <> Found "b")++testViewForm :: IO ()+testViewForm = do+  let html = runIdentity $ viewForm "f" (view ("hello" :: Text))+  assertEq "viewForm renders defaults" ("hello" :: Text) html++evalForm+  :: [(Text, Text)]+  -> Form (WithEnvironment Text Identity) Text Text view a+  -> Either view a+evalForm pairs form =+  runIdentity $ flip runReaderT lookupFn $ getWithEnvironment $ eitherForm "f" form+  where+    lookupFn fid = pure $ maybe Missing Found (lookup (encodeFormId fid) pairs)++evalFormTest+  :: [(Text, TestInput)]+  -> Form (WithEnvironment TestInput Identity) TestInput Text view a+  -> Either view a+evalFormTest pairs form =+  runIdentity $ flip runReaderT lookupFn $ getWithEnvironment $ eitherForm "f" form+  where+    lookupFn fid = pure $ maybe Missing Found (lookup (encodeFormId fid) pairs)++evalFormDefault+  :: Form (WithEnvironment Text Identity) Text Text view a+  -> Either view a+evalFormDefault form =+  runIdentity $ flip runReaderT (const (pure Default)) $ getWithEnvironment $ eitherForm "f" form++evalFormTestDefault+  :: Form (WithEnvironment TestInput Identity) TestInput Text view a+  -> Either view a+evalFormTestDefault form =+  runIdentity $ flip runReaderT (const (pure Default)) $ getWithEnvironment $ eitherForm "f" form++decodeSubmitted+  :: Text+  -> Form (WithEnvironment Text Identity) Text Text view a+  -> Either Text a+decodeSubmitted raw form =+  runIdentity $+    runReaderT+      (getWithEnvironment (formDecodeInput form raw))+      (const (pure (Found raw)))++textField+  :: Text+  -> Text+  -> Form (WithEnvironment Text Identity) Text Text Text Text+textField name initial =+  Named.input name Right (\_fid val -> val) initial++testNamedInput :: IO ()+testNamedInput = do+  let form = (,) <$> textField "first" "a" <*> textField "last" "b"+  assertEq "named input reads environment"+    (Right ("Ada", "Lovelace") :: Either Text (Text, Text))+    (evalForm [("first", "Ada"), ("last", "Lovelace")] form)+  assertEq "named input uses defaults when missing"+    (Left "ab" :: Either Text (Text, Text))+    (evalForm [] form)++unnamedText+  :: Text+  -> Form (WithEnvironment Text Identity) Text Text Text Text+unnamedText initial =+  Unnamed.input Right (\_fid val -> val) initial++testUnnamedInput :: IO ()+testUnnamedInput = do+  let form = (,) <$> unnamedText "a" <*> unnamedText "b"+  assertEq "unnamed input enumerates ids"+    (Right ("Ada", "Lovelace") :: Either Text (Text, Text))+    (evalForm [("f-val-0", "Ada"), ("f-val-1", "Lovelace")] form)++readInt :: Text -> Either Text Int+readInt t = maybe (Left "not an int") Right (readMaybe (T.unpack t))++testIreqIopt :: IO ()+testIreqIopt = do+  assertEq "ireq success"+    (Right (42 :: Int) :: Either Text Int)+    (evalForm [("age", "42")] (Named.ireq "age" readInt 0))+  assertEq "ireq missing"+    (Left (mempty :: Text) :: Either Text Int)+    (evalForm [] (Named.ireq "age" readInt 0))+  assertEq "iopt missing is Nothing"+    (Right Nothing :: Either Text (Maybe Int))+    (evalForm [] (Named.iopt "age" readInt Nothing))+  assertEq "iopt found"+    (Right (Just (7 :: Int)) :: Either Text (Maybe Int))+    (evalForm [("age", "7")] (Named.iopt "age" readInt Nothing))++testProofs :: IO ()+testProofs = do+  let parsed =+        transformEither+          (pure "12" :: Form Identity Text Text Text String)+          (\s -> if s == "12" then Right (12 :: Int) else Left ("bad" :: Text))+          (const 0)+  assertEq "transformEither success"+    (Right (12 :: Int))+    (runIdentity $ eitherForm "f" parsed)+  let dec = prove (pure "42" :: Form Identity Text Text Text String) (decimal T.pack 0)+  assertEq "decimal proof"+    (Right (42 :: Int))+    (runIdentity $ eitherForm "f" dec)+  let nonempty = prove (pure [1, 2 :: Int] :: Form Identity Text Text Text [Int]) (notNullProof ("empty" :: Text))+  assertEq "notNullProof"+    (Right [1, 2 :: Int])+    (runIdentity $ eitherForm "f" nonempty)++testProofDecode :: IO ()+testProofDecode = do+  let decoded = transformEither (textField "amount" "0") readInt (const 0)+  assertEq "prove decode valid"+    (Right (7 :: Int))+    (decodeSubmitted "7" decoded)+  assertEq "prove decode invalid"+    (Left ("not an int" :: Text))+    (decodeSubmitted "nope" decoded)+  assertEq "prove submit valid"+    (Right (9 :: Int))+    (evalForm [("amount", "9")] decoded)++pickLabels :: FormId -> [Named.Choice Text Text] -> Text+pickLabels _ = T.intercalate "," . map Named.choiceLabel++testHoistForm :: IO ()+testHoistForm = do+  let inner = view ("ok" :: Text) :: Form Identity Text Text Text ()+      outer = hoistForm id inner+  assertEq "hoistForm Identity"+    ("ok" :: Text)+    (runIdentity $ viewForm "f" outer)++testAlternative :: IO ()+testAlternative = do+  let failing = Named.ireq "x" readInt 0 :: Form (WithEnvironment Text Identity) Text Text Text Int+      fallback = pure (99 :: Int) :: Form (WithEnvironment Text Identity) Text Text Text Int+      combined = failing <|> fallback+  assertEq "Alternative fallback"+    (Right (99 :: Int))+    (evalForm [] combined)++testCatchFormError :: IO ()+testCatchFormError = do+  let form =+        catchFormError+          (const (0 :: Int))+          (Named.ireq "x" readInt 999 :: Form (WithEnvironment Text Identity) Text Text Text Int)+  assertEq "catchFormError recovery"+    (Right (0 :: Int))+    (evalForm [] form)++parseTags :: Text -> Either Text [Text]+parseTags inp = Right (T.words inp)++testInputMulti :: IO ()+testInputMulti = do+  let form =+        Named.inputMulti+          "tags"+          [("a", "alpha"), ("b", "beta")]+          parseTags+          pickLabels+          (== "a")+  assertEq "inputMulti default selection"+    (Right ["a"] :: Either Text [Text])+    (evalFormDefault form)+  assertEq "inputMulti submitted values"+    (Right ["b"] :: Either Text [Text])+    (evalForm [("tags", "b")] form)+  assertEq "inputMulti multiple submitted values"+    (Right ["a", "b"] :: Either Text [Text])+    (evalForm [("tags", "a b")] form)++testInputChoice :: IO ()+testInputChoice = do+  let form =+        Named.inputChoice+          "mode"+          (== "b")+          (("a", "A") :| [("b", "B")])+          (\inp -> Right inp)+          pickLabels+  assertEq "inputChoice default"+    (Right ("b" :: Text))+    (evalForm [] form)+  assertEq "inputChoice submitted"+    (Right ("a" :: Text))+    (evalForm [("mode", "a")] form)++testInputFile :: IO ()+testInputFile = do+  let form = Named.inputFile "upload" (\_ -> "widget" :: Text)+  assertEq "inputFile initial render"+    (Right (mempty :: Text))+    (evalFormTestDefault form)+  assertEq "inputFile missing on submit"+    (Left ("widget" :: Text))+    (evalFormTest [] form)+  assertEq "inputFile submitted"+    (Right ("payload" :: Text))+    (evalFormTest [("upload", TestInput "payload")] form)+  assertEq "inputFile decode"+    (Right ("payload" :: Text))+    (runIdentity $ runReaderT (getWithEnvironment (formDecodeInput form (TestInput "payload"))) (const (pure Default)))