-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathValidLiterals.hs
144 lines (132 loc) · 5.7 KB
/
ValidLiterals.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
-------------------------------------------------------------------------------
-- |
-- Module : ValidLiterals
-- Copyright : (C) 2015-2021 Merijn Verstraaten
-- License : BSD-style (see the file LICENSE)
-- Maintainer : Merijn Verstraaten <[email protected]>
-- Stability : experimental
-- Portability : portable
--
-- To disallow invalid input it is common to define (new)types with hidden
-- data constructors. Forcing the user to go through a smart-constructor that
-- enforces invariants and returns @Maybe ResultType@, preventing the
-- construction of data with invalid values.
--
-- However, it is __also__ common to want to include literal values of such
-- types in source text. Things of textual literals for HTML, HTTP, etc.
-- In such cases smart-constructors force us to handle potential conversion
-- failures at runtime, or abusing functions like @fromJust@ to break away all
-- the safety smart-constructors provide. All this despite the fact that we
-- can statically know at compile time that the conversion will always succeed
-- or always fails.
--
-- This package provides a typeclasses for using TH to validate the
-- correctness of provided literals at compile. This lets you define, e.g.,
-- @newtype Even = Even Integer@ and write:
--
-- @
-- x :: Even
-- x = $$(valid 38)
-- @
--
-- This will check, at compile time, that the provided 'Integer' is, in fact,
-- even and unwrap it from 'Maybe', avoiding the runtime check.
-------------------------------------------------------------------------------
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module ValidLiterals
( Validate(..)
, ValidationFailure(..)
, valid
, validInteger
, validRational
, validString
, validList
-- * Re-export from "Language.Haskell.TH.Syntax"
, Lift(..)
) where
import Control.Exception (Exception(displayException), throwIO)
import Data.Proxy (Proxy(Proxy))
import Data.Typeable (Typeable)
import Language.Haskell.TH.Syntax
import Language.Haskell.TH.Syntax.Compat (Splice, liftSplice)
-- | 'Exception' type for failed conversions. Useful for testing and more
-- gracefully handling compile time failures.
data ValidationFailure = ValidationFailure String deriving (Show, Typeable)
instance Exception ValidationFailure where
displayException (ValidationFailure s) = "Validation failure: " ++ s
-- | Class for validated, compile-time, partial conversions from type 'a' to
-- 'b'.
class Validate a b where
-- | Converts 'a' values into validated 'b' values, 'Left' values are
-- reported in the compilation error.
fromLiteralWithError :: a -> Either String b
fromLiteralWithError = maybe (Left errMsg) Right . fromLiteral
where
errMsg = "An error occured during compile-time validation!"
-- | Converts 'a' values into validated 'b' values, 'Nothing' values
-- produce a generic error message. Use 'fromLiteralWithError' for custom
-- error messages.
fromLiteral :: a -> Maybe b
fromLiteral = either (const Nothing) Just . fromLiteralWithError
{-# MINIMAL fromLiteralWithError | fromLiteral #-}
-- | Creates a Typed TH splice for the resulting 'b' values, useful for
-- avoiding the need for orphan 'Lift' instances and allowing complex
-- splices for types that can't be directly lifted. See the 'ByteString'
-- example module for an example.
liftResult :: Proxy a -> b -> Splice Q b
default liftResult :: Lift b => Proxy a -> b -> Splice Q b
liftResult _ val = [|| val ||]
-- | The core function of ValidLiterals, use this together with Typed Template
-- Haskell splices to insert validated literals into your code. For example, if
-- we assume @newtype ASCII = ASCII Char@ where @ASCII@ should only contain
-- ASCII characters, we would write:
--
-- Polymorphic literals, such as numbers (or strings when @OverloadedStrings@
-- is enabled) can result in ambiguous type errors with this function. Enabing
-- the @ExtendedDefaultRules@ extension will allow inputs to 'valid' to be
-- defaulted to 'Integer' or 'Double' allowing code to compile. A more robust
-- solution is to use the various explicitly defaulted functions in this
-- module, such as 'validInteger'.
--
-- @
-- {-\# LANGUAGE TemplateHaskell #-}
--
-- import ValidLiterals
--
-- x :: ASCII
-- x = $$(valid \'c\')
-- @
valid :: forall a b . Validate a b => a -> Splice Q b
valid input = case fromLiteralWithError input of
Right result -> liftResult (Proxy :: Proxy a) result
Left err -> liftSplice $ do
reportError $ unlines
[ "Invalid input used for type-safe validated literal!", err ]
runIO $ throwIO (ValidationFailure err)
-- | Integer literals lead to obnoxious defaulting complaints by GHC, by
-- using this function you can force the defaulting to 'Integer', silencing
-- these warnings.
--
-- Since 'Integral' literals use @fromInteger :: Num a => Integer -> a@ this
-- function cannot cost you any precision.
validInteger :: Validate Integer b => Integer -> Splice Q b
validInteger = valid
-- | Same as 'validInteger', but for 'Fractional' values.
--
-- Since 'Fractional' literals use
-- @fromRational :: Fractional a => Rational -> a@ this function cannot cost
-- you any precision.
validRational :: Validate Rational b => Rational -> Splice Q b
validRational = valid
-- | Same as 'validInteger', but for when enabling @OverloadedStrings@ makes
-- 'String' literals polymorphic.
validString :: Validate String b => String -> Splice Q b
validString = valid
-- | Same as 'validInteger', but for when enabling @OverloadedLists@ makes list
-- literals polymorphic.
validList :: Validate [a] b => [a] -> Splice Q b
validList = valid