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
|
||||
|
||||
import Control.Monad (when)
|
||||
import Data.Char (toLower)
|
||||
import Data.FileEmbed (makeRelativeToProject, embedStringFile)
|
||||
import Data.List (foldl', foldl1')
|
||||
-- import Data.String.Here (hereFile)
|
||||
import Data.List.Extra (foldl', foldl1', uncons, unsnoc)
|
||||
import qualified Data.Set as Set
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text.IO as T
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone,
|
||||
utcToLocalTime, utcToZonedTime)
|
||||
import Language.Haskell.TH (DecsQ, Name, mkName, nameBase)
|
||||
-- import Language.Haskell.TH.Quote (QuasiQuoter(..))
|
||||
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.FilePath (isRelative, (</>))
|
||||
import System.IO
|
||||
@ -243,6 +247,63 @@ embedFileRelative f = makeRelativeToProject f >>= embedStringFile
|
||||
-- where
|
||||
-- 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_Text
|
||||
]
|
||||
|
||||
@ -119,6 +119,7 @@ library
|
||||
, hashtables >=1.2.3.1
|
||||
, megaparsec >=7.0.0 && <9.2
|
||||
, microlens >=0.4
|
||||
, microlens-th >=0.4
|
||||
, mtl >=2.2.1
|
||||
, parser-combinators >=0.4.0
|
||||
, pretty-simple >4 && <5
|
||||
@ -169,6 +170,7 @@ test-suite doctest
|
||||
, hashtables >=1.2.3.1
|
||||
, megaparsec >=7.0.0 && <9.2
|
||||
, microlens >=0.4
|
||||
, microlens-th >=0.4
|
||||
, mtl >=2.2.1
|
||||
, parser-combinators >=0.4.0
|
||||
, pretty-simple >4 && <5
|
||||
@ -221,6 +223,7 @@ test-suite unittest
|
||||
, hledger-lib
|
||||
, megaparsec >=7.0.0 && <9.2
|
||||
, microlens >=0.4
|
||||
, microlens-th >=0.4
|
||||
, mtl >=2.2.1
|
||||
, parser-combinators >=0.4.0
|
||||
, pretty-simple >4 && <5
|
||||
|
||||
@ -53,6 +53,7 @@ dependencies:
|
||||
- hashtables >=1.2.3.1
|
||||
- megaparsec >=7.0.0 && <9.2
|
||||
- microlens >=0.4
|
||||
- microlens-th >=0.4
|
||||
- mtl >=2.2.1
|
||||
- parser-combinators >=0.4.0
|
||||
- pretty-simple >4 && <5
|
||||
|
||||
Loading…
Reference in New Issue
Block a user