-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathSegNode.hs
More file actions
310 lines (270 loc) · 10.5 KB
/
SegNode.hs
File metadata and controls
310 lines (270 loc) · 10.5 KB
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
{-# LANGUAGE TemplateHaskell #-}
-- For constraints on "extend"
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{-# OPTIONS_GHC -Wno-orphans #-}
#if defined(IS_WINDOWS)
#define OS_NAME Windows
#define OS_PATH WindowsPath
#else
#define OS_NAME Posix
#define OS_PATH PosixPath
#endif
-- |
-- Module : Streamly.Internal.FileSystem.OS_PATH.SegNode
-- Copyright : (c) 2023 Composewell Technologies
-- License : BSD3
-- Maintainer : streamly@composewell.com
-- Portability : GHC
--
-- When @Rooted/Branch@ and @File/Dir@ both are present, @Rooted/Branch@ must be
-- outermost constructors and @File/Dir@ as inner. Thus the types File (Rooted
-- a) or Dir (Rooted a) are not allowed but Rooted (Dir a) and Rooted (File a) are
-- allowed.
module Streamly.Internal.FileSystem.OS_PATH.SegNode
(
-- * Statically Verified Path Literals
-- | Quasiquoters.
rtdir
, brdir
, rtfile
, brfile
-- * Statically Verified Path Strings
-- | Template Haskell expression splices.
, rtdirE
, brdirE
, rtfileE
, brfileE
-- * Operations
, extend
)
where
import Language.Haskell.TH.Syntax (lift)
import Streamly.Internal.FileSystem.Path.Common (mkQ)
import Streamly.Internal.FileSystem.OS_PATH (OS_PATH(..))
import Streamly.Internal.FileSystem.OS_PATH.Seg (Rooted(..), Branch(..))
import Streamly.Internal.FileSystem.OS_PATH.Node (File(..), Dir(..))
import qualified Streamly.Internal.FileSystem.OS_PATH as OsPath
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Streamly.Internal.Data.Path
{- $setup
>>> :m
>>> :set -XQuasiQuotes
For APIs that have not been released yet.
>>> import Streamly.Internal.FileSystem.PosixPath (PosixPath)
>>> import Streamly.Internal.FileSystem.PosixPath.Node (Dir, File, dir, file)
>>> import Streamly.Internal.FileSystem.PosixPath.Seg (Rooted, Branch, rt, br)
>>> import Streamly.Internal.FileSystem.PosixPath.SegNode (rtdir, brdir, rtfile, brfile)
>>> import qualified Streamly.Internal.FileSystem.PosixPath as Path
>>> import qualified Streamly.Internal.FileSystem.PosixPath.SegNode as SegNode
-}
-- Note that (Rooted a) may also be a directory if "a" is (Dir b), but it can also
-- be a file if "a" is (File b). Therefore, the constraints are put on a more
-- specific type e.g. (Rooted OS_PATH) may be a dir.
{-
-- | Constraint to check if a type represents a directory.
class HasDir a
instance HasDir (Dir a)
instance HasDir (Rooted (Dir a))
instance HasDir (Branch (Dir a))
-}
-- Design notes:
--
-- There are two ways in which we can lift or upgrade a lower level path to a
-- higher level one. Lift each type directly from the base path e.g. Rooted (Dir
-- PosixPath) can be created directly from PosixPath. This allows us to do dir
-- checks and loc checks at the same time in a monolithic manner. But this also
-- makes us do the Dir checks again if we are lifting from Dir to Rooted. This
-- leads to less complicated constraints, more convenient type conversions.
--
-- Another alternative is to lift one segment at a time, so we lift PosixPath
-- to Dir and then Dir to Rooted. This way the checks are serialized, we perform
-- the dir checks first and then Rooted checks, we cannot combine them together.
-- The advantage is that when lifting from Dir to Rooted we do not need to do the
-- Dir checks. The disadvantage is less convenient conversion because of
-- stronger typing, we will need two steps - fromPath . fromPath and toPath .
-- toPath to upgrade or downgrade instead of just adapt.
--
{-
instance IsPath (File OS_PATH) (Rooted (File OS_PATH)) where
unsafeFromPath = Rooted
fromPath (File p) = do
_ :: Rooted OS_PATH <- fromPath p
pure $ Rooted (File p)
toPath (Rooted p) = p
instance IsPath (Rooted OS_PATH) (Rooted (File OS_PATH)) where
unsafeFromPath = Rooted
fromPath (File p) = do
_ :: File OS_PATH <- fromPath p
pure $ Rooted (File p)
toPath (Rooted p) = p
-}
-- Assuming that lifting from Dir/File to Rooted/Branch is not common and even if it
-- is then the combined cost of doing Dir/Rooted checks would be almost the same
-- as individual checks, we take the first approach.
instance IsPath OS_PATH (Rooted (File OS_PATH)) where
unsafeFromPath p = Rooted (File p)
fromPath p = do
_ :: File OS_PATH <- fromPath p
_ :: Rooted OS_PATH <- fromPath p
pure $ Rooted (File p)
toPath (Rooted (File p)) = p
instance IsPath OS_PATH (Rooted (Dir OS_PATH)) where
unsafeFromPath p = Rooted (Dir p)
fromPath p = do
_ :: Dir OS_PATH <- fromPath p
_ :: Rooted OS_PATH <- fromPath p
pure $ Rooted (Dir p)
toPath (Rooted (Dir p)) = p
instance IsPath OS_PATH (Branch (File OS_PATH)) where
unsafeFromPath p = Branch (File p)
fromPath p = do
_ :: File OS_PATH <- fromPath p
_ :: Branch OS_PATH <- fromPath p
pure $ Branch (File p)
toPath (Branch (File p)) = p
instance IsPath OS_PATH (Branch (Dir OS_PATH)) where
unsafeFromPath p = Branch (Dir p)
fromPath p = do
_ :: Dir OS_PATH <- fromPath p
_ :: Branch OS_PATH <- fromPath p
pure $ Branch (Dir p)
toPath (Branch (Dir p)) = p
------------------------------------------------------------------------------
-- Statically Verified Strings
------------------------------------------------------------------------------
-- XXX We can lift the array directly, ByteArray has a lift instance. Does that
-- work better?
liftRootedDir :: Rooted (Dir OS_PATH) -> Q Exp
liftRootedDir (Rooted (Dir p)) =
[| OsPath.unsafeFromString $(lift $ OsPath.toString p) :: Rooted (Dir OS_PATH)|]
liftBranchDir :: Branch (Dir OS_PATH) -> Q Exp
liftBranchDir (Branch (Dir p)) =
[| OsPath.unsafeFromString $(lift $ OsPath.toString p) :: Branch (Dir OS_PATH) |]
liftRootedFile :: Rooted (File OS_PATH) -> Q Exp
liftRootedFile (Rooted (File p)) =
[| OsPath.unsafeFromString $(lift $ OsPath.toString p) :: Rooted (File OS_PATH)|]
liftBranchFile :: Branch (File OS_PATH) -> Q Exp
liftBranchFile (Branch (File p)) =
[| OsPath.unsafeFromString $(lift $ OsPath.toString p) :: Branch (File OS_PATH)|]
-- | Generates a Haskell expression of type @Rooted (Dir OS_PATH)@.
--
rtdirE :: String -> Q Exp
rtdirE = either (error . show) liftRootedDir . OsPath.fromString
-- | Generates a Haskell expression of type @Branch (Dir OS_PATH)@.
--
brdirE :: String -> Q Exp
brdirE = either (error . show) liftBranchDir . OsPath.fromString
-- | Generates a Haskell expression of type @Rooted (File OS_PATH)@.
--
rtfileE :: String -> Q Exp
rtfileE = either (error . show) liftRootedFile . OsPath.fromString
-- | Generates a Haskell expression of type @Branch (File OS_PATH)@.
--
brfileE :: String -> Q Exp
brfileE = either (error . show) liftBranchFile . OsPath.fromString
------------------------------------------------------------------------------
-- Statically Verified Literals
------------------------------------------------------------------------------
-- XXX Define folds or parsers to parse the paths.
-- XXX Build these on top of the str quasiquoter so that we get interpolation
-- for free. Interpolated vars if any have to be of appropriate type depending
-- on the context so that we can splice them safely.
-- | Generates a @Rooted (Dir OS_PATH)@ type from a quoted literal.
--
-- >>> Path.toString ([rtdir|/usr|] :: Rooted (Dir PosixPath))
-- "/usr"
--
rtdir :: QuasiQuoter
rtdir = mkQ rtdirE
-- | Generates a @Branch (Dir OS_PATH)@ type from a quoted literal.
--
-- >>> Path.toString ([brdir|usr|] :: Branch (Dir PosixPath))
-- "usr"
--
brdir :: QuasiQuoter
brdir = mkQ brdirE
-- | Generates a @Rooted (File OS_PATH)@ type from a quoted literal.
--
-- >>> Path.toString ([rtfile|/x.txt|] :: Rooted (File PosixPath))
-- "/x.txt"
--
rtfile :: QuasiQuoter
rtfile = mkQ rtfileE
-- | Generates a @Branch (File OS_PATH)@ type from a quoted literal.
--
-- >>> Path.toString ([brfile|x.txt|] :: Branch (File PosixPath))
-- "x.txt"
--
brfile :: QuasiQuoter
brfile = mkQ brfileE
-- The only safety we need for paths is: (1) The first path can only be a Dir
-- type path, and (2) second path can only be a Branch path.
{-
-- If the first path is 'Rooted' then the return type is also 'Rooted'.
--
-- If the second path does not have 'File' or 'Dir' information then the return
-- type too cannot have it.
--
-- >> Path.toString (SegNode.extend [rtdir|/usr|] [br|bin|] :: Rooted PosixPath)
-- "/usr/bin"
-- >> Path.toString (SegNode.extend [brdir|usr|] [br|bin|] :: Branch PosixPath)
-- "usr/bin"
--
-- >> Path.toString (SegNode.extend [rt|/usr|] [br|bin|] :: Rooted PosixPath)
-- "/usr/bin"
-- >> Path.toString (SegNode.extend [br|usr|] [br|bin|] :: Branch PosixPath)
-- "usr/bin"
--
-- If the second path has 'File' or 'Dir' information then the return type
-- also has it.
--
-- >> Path.toString (SegNode.extend [rt|/usr|] [brdir|bin|] :: Rooted (Dir PosixPath))
-- "/usr/bin"
-- >> Path.toString (SegNode.extend [rt|/usr|] [brfile|bin|] :: Rooted (File PosixPath))
-- "/usr/bin"
-- >> Path.toString (SegNode.extend [br|usr|] [brdir|bin|] :: Branch (Dir PosixPath))
-- "usr/bin"
-- >> Path.toString (SegNode.extend [br|usr|] [brfile|bin|] :: Branch (File PosixPath))
-- "usr/bin"
--
-- Type error cases:
--
-- >> SegNode.extend [dir|/usr|] [br|bin|] -- first arg must be Rooted/Branch
-- >> SegNode.extend [file|/usr|] [br|bin|] -- first arg must be Rooted/Branch
-- >> SegNode.extend [rtfile|/usr|] [br|bin|] -- first arg must be a dir
-- >> SegNode.extend [rt|/usr|] [rt|/bin|] -- second arg must be seg
-- >> SegNode.extend [rt|/usr|] [dir|bin|] -- second arg must be seg
-- >> SegNode.extend [rt|/usr|] [file|bin|] -- second arg must be seg
--
{-# INLINE extend #-}
extend ::
(
IsSeg (a b)
, HasDir (a b)
, IsPath OS_PATH (a b)
, IsPath OS_PATH c
, IsPath OS_PATH (a c)
) => a b -> Branch c -> a c
extend a (Branch c) = unsafeFromPath $ OS_NAME.unsafeExtend (toPath a) (toPath c)
-}
-- | Append a branch type path to a directory.
--
-- >>> Path.toString (SegNode.extend [rtdir|/usr|] [brdir|bin|] :: Rooted (Dir PosixPath))
-- "/usr/bin"
-- >>> Path.toString (SegNode.extend [rtdir|/usr|] [brfile|bin|] :: Rooted (File PosixPath))
-- "/usr/bin"
-- >>> Path.toString (SegNode.extend [brdir|usr|] [brdir|bin|] :: Branch (Dir PosixPath))
-- "usr/bin"
-- >>> Path.toString (SegNode.extend [brdir|usr|] [brfile|bin|] :: Branch (File PosixPath))
-- "usr/bin"
--
{-# INLINE extend #-}
extend ::
(
IsPath OS_PATH (a (Dir OS_PATH))
, IsPath OS_PATH (b OS_PATH)
, IsPath OS_PATH (a (b OS_PATH))
) => a (Dir OS_PATH) -> Branch (b OS_PATH) -> a (b OS_PATH)
extend p1 (Branch p2) =
unsafeFromPath $ OsPath.unsafeExtend (toPath p1) (toPath p2)