-
Notifications
You must be signed in to change notification settings - Fork 0
/
Overview.hs
265 lines (228 loc) · 10.6 KB
/
Overview.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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-deprecations #-}
-- | type-encoding overview examples.
--
-- This library is concerned with 3 main operations done on strings:
-- __encoding__, __decoding__, and __recovery__. Examples in this module cover all
-- of these base cases.
--
-- This module uses encoding instances found in
--
-- * "Data.TypedEncoding.Instances.Enc.Base64"
-- * "Data.TypedEncoding.Instances.Restriction.ASCII"
-- * "Examples.TypedEncoding.Instances.Do.Sample"
--
module Examples.TypedEncoding.Overview where
import Data.TypedEncoding
import Data.TypedEncoding.Instances.Enc.Base64 ()
import Data.TypedEncoding.Instances.Enc.Warn.Base64 ()
import Data.TypedEncoding.Instances.Restriction.ASCII ()
import Examples.TypedEncoding.Instances.Do.Sample
import Examples.TypedEncoding.Util (HasA (..))
import qualified Data.ByteString as B
import qualified Data.Text as T
-- $setup
-- >>> :set -XOverloadedStrings -XMultiParamTypeClasses -XDataKinds -XTypeApplications
-- >>> import Data.Functor.Identity
--
-- This module contains some ghci friendly values to play with.
--
-- Each value is documented in a doctest style by including an equivalent ghci ready expression.
-- These documents generate a test suite for this library as well.
-- * Basics
-- | "Hello World" encoded as Base64
--
-- >>> helloB64
-- UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ="
--
-- >>> displ helloB64
-- "Enc '[enc-B64] () (ByteString SGVsbG8gV29ybGQ=)"
--
-- >>> encodeAll . toEncoding () $ "Hello World" :: Enc '["enc-B64"] () B.ByteString
-- UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ="
helloB64 :: Enc '["enc-B64"] () B.ByteString
helloB64 = encodeAll . toEncoding () $ "Hello World"
-- | "Hello World" double-Base64 encoded.
-- Notice the same code used as in single encoding, the game is played at type level.
--
-- >>> encodeAll . toEncoding () $ "Hello World" :: Enc '["enc-B64","enc-B64"] () B.ByteString
-- UnsafeMkEnc Proxy () "U0dWc2JHOGdWMjl5YkdRPQ=="
--
-- >>> displ helloB64B64
-- "Enc '[enc-B64,enc-B64] () (ByteString U0dWc2JHOGdWMjl5YkdRPQ==)"
--
-- An alternative version of the above code is this:
--
-- >>> fmap displ . runEncodings' @'["enc-B64","enc-B64"] @'["enc-B64","enc-B64"] @Identity encodings . toEncoding () $ ("Hello World" :: B.ByteString)
-- Identity "Enc '[enc-B64,enc-B64] () (ByteString U0dWc2JHOGdWMjl5YkdRPQ==)"
--
-- This is how /typed-encoding/ works, the "Data.TypedEncoding.Common.Class.Encode.EncodeAll"
-- constraint can be used to get access to list to encodings required by the symbol annotation.
-- 'runEncodings'' executes all the necessary transformations.
--
-- Similar story is true for /decoding/ and /validation/. In these examples we will use shortcut combinators.
helloB64B64 :: Enc '["enc-B64","enc-B64"] () B.ByteString
helloB64B64 = encodeAll . toEncoding () $ "Hello World"
-- | Previous text decoded from Base64
--
-- >>> fromEncoding . decodeAll $ helloB64
-- "Hello World"
helloB64Decoded :: B.ByteString
helloB64Decoded = fromEncoding . decodeAll $ helloB64
-- | 'recreateFAll' allows for recovering data at program boundaries (for example, when parsing JSON input).
-- It makes sure that the content satisfies specified encodings.
--
-- >>> recreateFAll . toEncoding () $ "SGVsbG8gV29ybGQ=" :: Either RecreateEx (Enc '["enc-B64"] () B.ByteString)
-- Right (UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ=")
--
-- >>> recreateFAll . toEncoding () $ "SGVsbG8gV29ybGQ" :: Either RecreateEx (Enc '["enc-B64"] () B.ByteString)
-- Left (RecreateEx "enc-B64" ("Base64-encoded bytestring is unpadded or has invalid padding"))
--
-- The above example start by placing payload in zero-encoded @Enc '[] ()@ type and then apply 'recreateFAll'
-- this is a good way to recreate encoded type if encoding is known.
--
-- If is it not, 'UncheckedEnc' type can be used.
--
-- (See 'Examples.TypedEncoding.ToEncString' for better example).
--
-- This module is concerned only with the first approach.
--
-- >>> let unchecked = toUncheckedEnc ["enc-B64"] () ("SGVsbG8gV29ybGQ=" :: T.Text)
-- >>> check @'["enc-B64"] @(Either RecreateEx) unchecked
-- Just (Right (UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ="))
helloB64Recovered :: Either RecreateEx (Enc '["enc-B64"] () B.ByteString)
helloB64Recovered = recreateFAll . toEncoding () $ "SGVsbG8gV29ybGQ="
-- | Double Base64 encoded "Hello World" with one layer of encoding removed
--
-- >>> decodePart @'["enc-B64"] $ helloB64B64 :: Enc '["enc-B64"] () B.ByteString
-- UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ="
--
-- >>> helloB64B64PartDecode == helloB64
-- True
--
-- @decodePart@ is a convenience function that simply replies decoding 'above' first "enc-B64"
--
-- >>> above @'["enc-B64"] @'["enc-B64"] @'[] decodeAll $ helloB64B64
-- UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ="
helloB64B64PartDecode :: Enc '["enc-B64"] () B.ByteString
helloB64B64PartDecode = decodePart @'["enc-B64"] helloB64B64
-- | 'helloB64B64' all the way to 'B.ByteString'
--
-- Notice a similar polymorphism in decoding.
--
-- >>> fromEncoding . decodeAll $ helloB64B64 :: B.ByteString
-- "Hello World"
--
-- We can also decode all the parts:
--
-- >>> fromEncoding . decodePart @'["enc-B64","enc-B64"] $ helloB64B64
-- "Hello World"
helloB64B64Decoded :: B.ByteString
helloB64B64Decoded = fromEncoding . decodeAll $ helloB64B64
-- | what happens when we try to recover encoded once text to @Enc '["enc-B64", "enc-B64"]@.
--
-- Again, notice the same expression is used as in previous recovery.
--
-- >>> recreateFAll . toEncoding () $ "SGVsbG8gV29ybGQ=" :: Either RecreateEx (Enc '["enc-B64", "enc-B64"] () B.ByteString)
-- Left (RecreateEx "enc-B64" ("Base64-encoded bytestring is unpadded or has invalid padding"))
helloB64B64RecoveredErr :: Either RecreateEx (Enc '["enc-B64", "enc-B64"] () B.ByteString)
helloB64B64RecoveredErr = recreateFAll . toEncoding () $ "SGVsbG8gV29ybGQ="
-- * "do-" Encodings
-- |
-- "do-UPPER" (from 'Examples.TypedEncoding.Instances.Do.Sample' module) encoding applied to "Hello World"
--
-- Notice a namespace thing going on, "enc-" is encoding, "do-" is some transformation.
-- These are typically not reversible, some could be recoverable.
--
-- The same code is used as in "enc-" examples to encode (now transform).
--
-- >>> encodeAll . toEncoding () $ "Hello World" :: Enc '["do-UPPER"] () T.Text
-- UnsafeMkEnc Proxy () "HELLO WORLD"
helloUPP :: Enc '["do-UPPER"] () T.Text
helloUPP = encodeAll . toEncoding () $ "Hello World"
-- | Sample compound transformation
--
-- >>> encodeAll . toEncoding () $ "HeLLo world" :: Enc '["do-reverse", "do-Title"] () T.Text
-- UnsafeMkEnc Proxy () "dlroW olleH"
helloTitleRev :: Enc '["do-reverse", "do-Title"] () T.Text
helloTitleRev = encodeAll . toEncoding () $ "HeLLo world"
-- * Configuration
-- | Example configuration
newtype Config = Config {
sizeLimit :: SizeLimit
} deriving (Show)
exampleConf = Config (SizeLimit 8)
instance HasA SizeLimit Config where
has = sizeLimit
-- | @helloTitle'@ is needed in following examples
--
helloTitle :: Enc '["do-Title"] Config T.Text
helloTitle = encodeAll . toEncoding exampleConf $ "hello wOrld"
-- | Configuration can be used to impact the encoding process.
--
-- So far we had used @()@ as configuration of all encodings.
-- But since both "do-reverse", "do-Title" are polymorphic in
-- configuration we can also do this:
--
-- >>> encodeAll . toEncoding exampleConf $ "HeLLo world" :: Enc '["do-reverse", "do-Title"] Config T.Text
-- UnsafeMkEnc Proxy (Config {sizeLimit = SizeLimit {unSizeLimit = 8}}) "dlroW olleH"
--
-- >>> encodeAll . toEncoding exampleConf $ "HeLlo world" :: Enc '["do-size-limit", "do-reverse", "do-Title"] Config T.Text
-- UnsafeMkEnc Proxy (Config {sizeLimit = SizeLimit {unSizeLimit = 8}}) "dlroW ol"
--
-- Instead, encode previously defined 'helloTitle' by reversing it and adding size limit
--
-- >>> encodePart @'["do-size-limit", "do-reverse"] helloTitle :: Enc '["do-size-limit", "do-reverse", "do-Title"] Config T.Text
-- UnsafeMkEnc Proxy (Config {sizeLimit = SizeLimit {unSizeLimit = 8}}) "dlroW ol"
--
-- @encodePart@ is simply encodeAll played above "do-Title" encoding:
--
-- >>> above @'["do-Title"] @'[] @'["do-size-limit", "do-reverse"] encodeAll helloTitle
-- UnsafeMkEnc Proxy (Config {sizeLimit = SizeLimit {unSizeLimit = 8}}) "dlroW ol"
helloRevLimit :: Enc '["do-size-limit", "do-reverse", "do-Title"] Config T.Text
helloRevLimit = encodePart @'["do-size-limit", "do-reverse"] helloTitle
-- >>> encodeAll . toEncoding exampleConf $ "HeLlo world" :: Enc '["enc-B64", "do-size-limit"] Config B.ByteString
-- UnsafeMkEnc Proxy (Config {sizeLimit = SizeLimit {unSizeLimit = 8}}) "SGVMbG8gd28="
helloLimitB64 :: Enc '["enc-B64", "do-size-limit"] Config B.ByteString
helloLimitB64 = encodeAll . toEncoding exampleConf $ "HeLlo world"
-- | ... and we unwrap the B64 part only
--
-- >>> decodePart @'["enc-B64"] $ helloLimitB64
-- UnsafeMkEnc Proxy (Config {sizeLimit = SizeLimit {unSizeLimit = 8}}) "HeLlo wo"
helloRevLimitParDec :: Enc '["do-size-limit"] Config B.ByteString
helloRevLimitParDec = decodePart @'["enc-B64"] helloLimitB64
-- * "r-" encodings section
-- | ASCII char set
-- ByteStrings are sequences of Bytes ('Data.Word.Word8'). The type
-- is very permissive, it may contain binary data such as jpeg picture.
--
-- "r-ASCII" encoding acts as partial identity function
-- it does not change any bytes in bytestring but it fails if a byte
-- is outside of ASCII range (in @Either@ monad).
--
-- Note naming thing: "r-" is partial identity ("r-" is from restriction).
--
-- >>> _runEncodings encodings . toEncoding () $ "HeLlo world" :: Either EncodeEx (Enc '["r-ASCII"] () B.ByteString)
-- Right (UnsafeMkEnc Proxy () "HeLlo world")
--
-- or equivalently
--
-- >>> encodeFAll . toEncoding () $ "HeLlo world" :: Either EncodeEx (Enc '["r-ASCII"] () B.ByteString)
-- Right (UnsafeMkEnc Proxy () "HeLlo world")
helloAscii :: Either EncodeEx (Enc '["r-ASCII"] () B.ByteString)
helloAscii = encodeFAll . toEncoding () $ "HeLlo world"
-- | Arguably the type we used for helloB64 was too permissive.
-- a better version is here:
--
-- >>> encodeFAll . toEncoding () $ "Hello World" :: Either EncodeEx (Enc '["enc-B64", "r-ASCII"] () B.ByteString)
-- Right (UnsafeMkEnc Proxy () "SGVsbG8gV29ybGQ=")
helloAsciiB64 :: Either EncodeEx (Enc '["enc-B64", "r-ASCII"] () B.ByteString)
helloAsciiB64 = encodeFAll . toEncoding () $ "Hello World"
-- |
-- >>> decodePart @'["enc-B64"] <$> helloAsciiB64
-- Right (UnsafeMkEnc Proxy () "Hello World")
helloAsciiB64PartDec :: Either EncodeEx (Enc '["r-ASCII"] () B.ByteString)
helloAsciiB64PartDec = decodePart @'["enc-B64"] <$> helloAsciiB64