dev: lens: Create a Template Haskell helper for generating classy lenses
for hledger options. This works for BalancingOpts, InputOpts, ReportOpts, ReportSpec, and CliOpts.
This commit is contained in:
parent
13206d0b18
commit
c784da3d0c
@ -37,16 +37,20 @@ module Hledger.Utils (---- provide these frequently used modules - or not, for c
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Control.Monad (when)
|
import Control.Monad (when)
|
||||||
|
import Data.Char (toLower)
|
||||||
import Data.FileEmbed (makeRelativeToProject, embedStringFile)
|
import Data.FileEmbed (makeRelativeToProject, embedStringFile)
|
||||||
import Data.List (foldl', foldl1')
|
import Data.List.Extra (foldl', foldl1', uncons, unsnoc)
|
||||||
-- import Data.String.Here (hereFile)
|
import qualified Data.Set as Set
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text.IO as T
|
import qualified Data.Text.IO as T
|
||||||
import Data.Time.Clock (getCurrentTime)
|
import Data.Time.Clock (getCurrentTime)
|
||||||
import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone,
|
import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone,
|
||||||
utcToLocalTime, utcToZonedTime)
|
utcToLocalTime, utcToZonedTime)
|
||||||
|
import Language.Haskell.TH (DecsQ, Name, mkName, nameBase)
|
||||||
-- import Language.Haskell.TH.Quote (QuasiQuoter(..))
|
-- import Language.Haskell.TH.Quote (QuasiQuoter(..))
|
||||||
import Language.Haskell.TH.Syntax (Q, Exp)
|
import Language.Haskell.TH.Syntax (Q, Exp)
|
||||||
|
import Lens.Micro ((&), (.~))
|
||||||
|
import Lens.Micro.TH (DefName(TopName), lensClass, lensField, makeLensesWith, classyRules)
|
||||||
import System.Directory (getHomeDirectory)
|
import System.Directory (getHomeDirectory)
|
||||||
import System.FilePath (isRelative, (</>))
|
import System.FilePath (isRelative, (</>))
|
||||||
import System.IO
|
import System.IO
|
||||||
@ -243,6 +247,63 @@ embedFileRelative f = makeRelativeToProject f >>= embedStringFile
|
|||||||
-- where
|
-- where
|
||||||
-- QuasiQuoter{quoteExp=hereFileExp} = hereFile
|
-- QuasiQuoter{quoteExp=hereFileExp} = hereFile
|
||||||
|
|
||||||
|
|
||||||
|
-- | Make classy lenses for Hledger options fields.
|
||||||
|
-- This is intended to be used with BalancingOpts, InputOpt, ReportOpts,
|
||||||
|
-- ReportSpec, and CliOpts.
|
||||||
|
-- When run on X, it will create a typeclass named HasX (except for ReportOpts,
|
||||||
|
-- which will be named HasReportOptsNoUpdate) containing all the lenses for that type.
|
||||||
|
-- If the field name starts with an underscore, the lens name will be created
|
||||||
|
-- by stripping the underscore from the front on the name. If the field name ends with
|
||||||
|
-- an underscore, the field name ends with an underscore, the lens name will be
|
||||||
|
-- mostly created by stripping the underscore, but a few names for which this
|
||||||
|
-- would create too many conflicts instead have a second underscore appended.
|
||||||
|
-- ReportOpts fields for which updating them requires updating the query in
|
||||||
|
-- ReportSpec are instead names by dropping the trailing underscore and
|
||||||
|
-- appending NoUpdate to the name, e.g. querystring_ -> querystringNoUpdate.
|
||||||
|
--
|
||||||
|
-- There are a few reasons for the complicated rules.
|
||||||
|
-- - We have some legacy field names ending in an underscore (e.g. value_)
|
||||||
|
-- which we want to temporarily accommodate, before eventually switching to
|
||||||
|
-- a more modern style (e.g. _rsReportOpts)
|
||||||
|
-- - Certain fields in ReportOpts need to update the enclosing ReportSpec when
|
||||||
|
-- they are updated, and it is a common programming error to forget to do
|
||||||
|
-- this. We append NoUpdate to those lenses which will not update the
|
||||||
|
-- enclosing field, and reserve the shorter name for manually define lenses
|
||||||
|
-- (or at least something lens-like) which will update the ReportSpec.
|
||||||
|
-- cf. the lengthy discussion here and in surrounding comments:
|
||||||
|
-- https://github.com/simonmichael/hledger/pull/1545#issuecomment-881974554
|
||||||
|
makeHledgerClassyLenses :: Name -> DecsQ
|
||||||
|
makeHledgerClassyLenses x = flip makeLensesWith x $ classyRules
|
||||||
|
& lensField .~ (\_ _ n -> fieldName $ nameBase n)
|
||||||
|
& lensClass .~ (className . nameBase)
|
||||||
|
where
|
||||||
|
fieldName n | Just ('_', name) <- uncons n = [TopName (mkName name)]
|
||||||
|
| Just (name, '_') <- unsnoc n,
|
||||||
|
name `Set.member` queryFields = [TopName (mkName $ name ++ "NoUpdate")]
|
||||||
|
| Just (name, '_') <- unsnoc n,
|
||||||
|
name `Set.member` commonFields = [TopName (mkName $ name ++ "__")]
|
||||||
|
| Just (name, '_') <- unsnoc n = [TopName (mkName name)]
|
||||||
|
| otherwise = []
|
||||||
|
|
||||||
|
-- Fields which would cause too many conflicts if we exposed lenses with these names.
|
||||||
|
commonFields = Set.fromList
|
||||||
|
[ "empty", "drop", "color", "transpose" -- ReportOpts
|
||||||
|
, "anon", "new", "auto" -- InputOpts
|
||||||
|
, "rawopts", "file", "debug", "width" -- CliOpts
|
||||||
|
]
|
||||||
|
|
||||||
|
-- When updating some fields of ReportOpts within a ReportSpec, we need to
|
||||||
|
-- update the rsQuery term as well. To do this we implement a special
|
||||||
|
-- HasReportOpts class with some special behaviour. We therefore give the
|
||||||
|
-- basic lenses a special NoUpdate name to avoid conflicts.
|
||||||
|
className "ReportOpts" = Just (mkName "HasReportOptsNoUpdate", mkName "reportOptsNoUpdate")
|
||||||
|
className (x:xs) = Just (mkName ("Has" ++ x:xs), mkName (toLower x : xs))
|
||||||
|
className [] = Nothing
|
||||||
|
|
||||||
|
-- Fields of ReportOpts which need to update the Query when they are updated.
|
||||||
|
queryFields = Set.fromList ["period", "statuses", "depth", "date2", "real", "querystring"]
|
||||||
|
|
||||||
tests_Utils = tests "Utils" [
|
tests_Utils = tests "Utils" [
|
||||||
tests_Text
|
tests_Text
|
||||||
]
|
]
|
||||||
|
|||||||
@ -119,6 +119,7 @@ library
|
|||||||
, hashtables >=1.2.3.1
|
, hashtables >=1.2.3.1
|
||||||
, megaparsec >=7.0.0 && <9.2
|
, megaparsec >=7.0.0 && <9.2
|
||||||
, microlens >=0.4
|
, microlens >=0.4
|
||||||
|
, microlens-th >=0.4
|
||||||
, mtl >=2.2.1
|
, mtl >=2.2.1
|
||||||
, parser-combinators >=0.4.0
|
, parser-combinators >=0.4.0
|
||||||
, pretty-simple >4 && <5
|
, pretty-simple >4 && <5
|
||||||
@ -169,6 +170,7 @@ test-suite doctest
|
|||||||
, hashtables >=1.2.3.1
|
, hashtables >=1.2.3.1
|
||||||
, megaparsec >=7.0.0 && <9.2
|
, megaparsec >=7.0.0 && <9.2
|
||||||
, microlens >=0.4
|
, microlens >=0.4
|
||||||
|
, microlens-th >=0.4
|
||||||
, mtl >=2.2.1
|
, mtl >=2.2.1
|
||||||
, parser-combinators >=0.4.0
|
, parser-combinators >=0.4.0
|
||||||
, pretty-simple >4 && <5
|
, pretty-simple >4 && <5
|
||||||
@ -221,6 +223,7 @@ test-suite unittest
|
|||||||
, hledger-lib
|
, hledger-lib
|
||||||
, megaparsec >=7.0.0 && <9.2
|
, megaparsec >=7.0.0 && <9.2
|
||||||
, microlens >=0.4
|
, microlens >=0.4
|
||||||
|
, microlens-th >=0.4
|
||||||
, mtl >=2.2.1
|
, mtl >=2.2.1
|
||||||
, parser-combinators >=0.4.0
|
, parser-combinators >=0.4.0
|
||||||
, pretty-simple >4 && <5
|
, pretty-simple >4 && <5
|
||||||
|
|||||||
@ -53,6 +53,7 @@ dependencies:
|
|||||||
- hashtables >=1.2.3.1
|
- hashtables >=1.2.3.1
|
||||||
- megaparsec >=7.0.0 && <9.2
|
- megaparsec >=7.0.0 && <9.2
|
||||||
- microlens >=0.4
|
- microlens >=0.4
|
||||||
|
- microlens-th >=0.4
|
||||||
- mtl >=2.2.1
|
- mtl >=2.2.1
|
||||||
- parser-combinators >=0.4.0
|
- parser-combinators >=0.4.0
|
||||||
- pretty-simple >4 && <5
|
- pretty-simple >4 && <5
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user