Skip to content

Commit f59ea9a

Browse files
hdgarroodpaf31
authored andcommitted
Add a newtype for package names (#29)
Part of #21; this does not fully fix #21 as it only addresses package names.
1 parent 73e81bf commit f59ea9a

File tree

3 files changed

+126
-23
lines changed

3 files changed

+126
-23
lines changed

app/Main.hs

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import qualified System.Process as Process
3535
import qualified Text.ParserCombinators.ReadP as Read
3636
import Turtle hiding (echo, fold, s, x)
3737
import qualified Turtle
38+
import Types (PackageName, mkPackageName, runPackageName, untitledPackageName, preludePackageName)
3839

3940
echoT :: Text -> IO ()
4041
echoT = Turtle.printf (Turtle.s % "\n")
@@ -43,19 +44,19 @@ packageFile :: Path.FilePath
4344
packageFile = "psc-package.json"
4445

4546
data PackageConfig = PackageConfig
46-
{ name :: Text
47-
, depends :: [Text]
47+
{ name :: PackageName
48+
, depends :: [PackageName]
4849
, set :: Text
4950
, source :: Text
5051
} deriving (Show, Generic, Aeson.FromJSON, Aeson.ToJSON)
5152

5253
pathToTextUnsafe :: Turtle.FilePath -> Text
5354
pathToTextUnsafe = either (error "Path.toText failed") id . Path.toText
5455

55-
defaultPackage :: Version -> Text -> PackageConfig
56+
defaultPackage :: Version -> PackageName -> PackageConfig
5657
defaultPackage pursVersion pkgName =
5758
PackageConfig { name = pkgName
58-
, depends = [ "prelude" ]
59+
, depends = [ preludePackageName ]
5960
, set = "psc-" <> pack (showVersion pursVersion)
6061
, source = "https://github.com/purescript/package-sets.git"
6162
}
@@ -104,10 +105,10 @@ writePackageFile =
104105
data PackageInfo = PackageInfo
105106
{ repo :: Text
106107
, version :: Text
107-
, dependencies :: [Text]
108+
, dependencies :: [PackageName]
108109
} deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)
109110

110-
type PackageSet = Map.Map Text PackageInfo
111+
type PackageSet = Map.Map PackageName PackageInfo
111112

112113
cloneShallow
113114
:: Text
@@ -165,20 +166,20 @@ writePackageSet PackageConfig{ set } =
165166
let dbFile = ".psc-package" </> fromText set </> ".set" </> "packages.json"
166167
in writeTextFile dbFile . packageSetToJSON
167168

168-
installOrUpdate :: Text -> Text -> PackageInfo -> IO Turtle.FilePath
169+
installOrUpdate :: Text -> PackageName -> PackageInfo -> IO Turtle.FilePath
169170
installOrUpdate set pkgName PackageInfo{ repo, version } = do
170-
echoT ("Updating " <> pkgName)
171-
let pkgDir = ".psc-package" </> fromText set </> fromText pkgName </> fromText version
171+
echoT ("Updating " <> runPackageName pkgName)
172+
let pkgDir = ".psc-package" </> fromText set </> fromText (runPackageName pkgName) </> fromText version
172173
exists <- testdir pkgDir
173174
unless exists . void $ cloneShallow repo version pkgDir
174175
pure pkgDir
175176

176-
getTransitiveDeps :: PackageSet -> [Text] -> IO [(Text, PackageInfo)]
177+
getTransitiveDeps :: PackageSet -> [PackageName] -> IO [(PackageName, PackageInfo)]
177178
getTransitiveDeps db depends = do
178179
pkgs <- for depends $ \pkg ->
179180
case Map.lookup pkg db of
180181
Nothing -> do
181-
echoT ("Package " <> pkg <> " does not exist in package set")
182+
echoT ("Package " <> runPackageName pkg <> " does not exist in package set")
182183
exit (ExitFailure 1)
183184
Just PackageInfo{ dependencies } -> return (pkg : dependencies)
184185
let unique = Set.toList (foldMap Set.fromList pkgs)
@@ -211,42 +212,57 @@ initialize = do
211212
echoT "psc-package.json already exists"
212213
exit (ExitFailure 1)
213214
echoT "Initializing new project in current directory"
214-
pkgName <- pathToTextUnsafe . Path.filename <$> pwd
215+
pkgName <- packageNameFromPWD . pathToTextUnsafe . Path.filename <$> pwd
215216
pursVersion <- getPureScriptVersion
216217
echoT ("Using the default package set for PureScript compiler version " <>
217218
fromString (showVersion pursVersion))
218219
let pkg = defaultPackage pursVersion pkgName
219220
writePackageFile pkg
220221
updateImpl pkg
221222

223+
where
224+
packageNameFromPWD =
225+
either (const untitledPackageName) id . mkPackageName
226+
222227
update :: IO ()
223228
update = do
224229
pkg <- readPackageFile
225230
updateImpl pkg
226231
echoT "Update complete"
227232

228233
install :: String -> IO ()
229-
install pkgName = do
234+
install pkgName' = do
230235
pkg <- readPackageFile
231-
let pkg' = pkg { depends = nub (pack pkgName : depends pkg) }
236+
pkgName <- packageNameFromString pkgName'
237+
let pkg' = pkg { depends = nub (pkgName : depends pkg) }
232238
updateImpl pkg'
233239
writePackageFile pkg'
234240
echoT "psc-package.json file was updated"
235241

236242
uninstall :: String -> IO ()
237-
uninstall pkgName = do
243+
uninstall pkgName' = do
238244
pkg <- readPackageFile
239-
let pkg' = pkg { depends = filter (/= pack pkgName) $ depends pkg }
245+
pkgName <- packageNameFromString pkgName'
246+
let pkg' = pkg { depends = filter (/= pkgName) $ depends pkg }
240247
updateImpl pkg'
241248
writePackageFile pkg'
242249
echoT "psc-package.json file was updated"
243250

251+
packageNameFromString :: String -> IO PackageName
252+
packageNameFromString str =
253+
case mkPackageName (pack str) of
254+
Right pkgName ->
255+
pure pkgName
256+
Left _ -> do
257+
echoT ("Invalid package name: " <> pack (show str))
258+
exit (ExitFailure 1)
259+
244260
listDependencies :: IO ()
245261
listDependencies = do
246262
pkg@PackageConfig{ depends } <- readPackageFile
247263
db <- readPackageSet pkg
248264
trans <- getTransitiveDeps db depends
249-
traverse_ (echoT . fst) trans
265+
traverse_ (echoT . runPackageName . fst) trans
250266

251267
listPackages :: Bool -> IO ()
252268
listPackages sorted = do
@@ -256,8 +272,9 @@ listPackages sorted = do
256272
then traverse_ echoT (fmt <$> inOrder (Map.assocs db))
257273
else traverse_ echoT (fmt <$> Map.assocs db)
258274
where
259-
fmt :: (Text, PackageInfo) -> Text
260-
fmt (name, PackageInfo{ version, repo }) = name <> " (" <> version <> ", " <> repo <> ")"
275+
fmt :: (PackageName, PackageInfo) -> Text
276+
fmt (name, PackageInfo{ version, repo }) =
277+
runPackageName name <> " (" <> version <> ", " <> repo <> ")"
261278

262279
inOrder xs = fromNode . fromVertex <$> vs where
263280
(gr, fromVertex) =
@@ -267,12 +284,12 @@ listPackages sorted = do
267284
vs = G.topSort (G.transposeG gr)
268285
fromNode (pkg, name, _) = (name, pkg)
269286

270-
getSourcePaths :: PackageConfig -> PackageSet -> [Text] -> IO [Turtle.FilePath]
287+
getSourcePaths :: PackageConfig -> PackageSet -> [PackageName] -> IO [Turtle.FilePath]
271288
getSourcePaths PackageConfig{..} db pkgNames = do
272289
trans <- getTransitiveDeps db pkgNames
273290
let paths = [ ".psc-package"
274291
</> fromText set
275-
</> fromText pkgName
292+
</> fromText (runPackageName pkgName)
276293
</> fromText version
277294
</> "src" </> "**" </> "*.purs"
278295
| (pkgName, PackageInfo{ version }) <- trans
@@ -315,7 +332,7 @@ checkForUpdates applyMinorUpdates applyMajorUpdates = do
315332
echoT "Warning: this could take some time!"
316333

317334
newDb <- Map.fromList <$> (for (Map.toList db) $ \(name, p@PackageInfo{ repo, version }) -> do
318-
echoT ("Checking package " <> name)
335+
echoT ("Checking package " <> runPackageName name)
319336
tagLines <- Turtle.fold (listRemoteTags repo) Foldl.list
320337
let tags = mapMaybe parseTag tagLines
321338
newVersion <- case parsePackageVersion version of
@@ -397,7 +414,7 @@ verifyPackageSet = do
397414

398415
for_ (Map.toList db) $ \(name, PackageInfo{..}) -> do
399416
let dirFor pkgName = fromMaybe (error ("verifyPackageSet: no directory for " <> show pkgName)) (Map.lookup pkgName paths)
400-
echoT ("Verifying package " <> name)
417+
echoT ("Verifying package " <> runPackageName name)
401418
let srcGlobs = map (pathToTextUnsafe . (</> ("src" </> "**" </> "*.purs")) . dirFor) (name : dependencies)
402419
procs "purs" ("compile" : srcGlobs) empty
403420

app/Types.hs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Types
4+
( PackageName
5+
, mkPackageName
6+
, runPackageName
7+
, preludePackageName
8+
, untitledPackageName
9+
) where
10+
11+
import Control.Category ((>>>))
12+
import Data.Aeson (FromJSON, ToJSON, FromJSONKey(..), ToJSONKey(..), ToJSONKeyFunction(..), FromJSONKeyFunction(..), parseJSON, toJSON, withText)
13+
import qualified Data.Aeson.Encoding as AesonEncoding
14+
import Data.Char (isAscii, isLower, isDigit)
15+
import Data.Monoid ((<>))
16+
import Data.Text (Text)
17+
import qualified Data.Text as T
18+
19+
newtype PackageName
20+
= PackageName Text
21+
deriving (Show, Eq, Ord)
22+
23+
instance ToJSON PackageName where
24+
toJSON (PackageName t) = toJSON t
25+
26+
instance FromJSON PackageName where
27+
parseJSON =
28+
withText "package name" fromText
29+
30+
fromText :: Monad m => Text -> m PackageName
31+
fromText t =
32+
case mkPackageName t of
33+
Right pkgName -> pure pkgName
34+
Left errs -> fail $ "Invalid package name: " <> show errs
35+
36+
instance ToJSONKey PackageName where
37+
toJSONKey =
38+
ToJSONKeyText
39+
runPackageName
40+
(AesonEncoding.text . runPackageName)
41+
42+
instance FromJSONKey PackageName where
43+
fromJSONKey =
44+
FromJSONKeyTextParser fromText
45+
46+
data PackageNameError
47+
= NotEmpty
48+
| TooLong Int
49+
| InvalidChars [Char]
50+
| RepeatedSeparators
51+
| MustNotBeginSeparator
52+
| MustNotEndSeparator
53+
deriving (Show, Eq, Ord)
54+
55+
-- | Smart constructor for package names. Based on Bower's requirements for
56+
-- | package names.
57+
mkPackageName :: Text -> Either PackageNameError PackageName
58+
mkPackageName = fmap PackageName . validateAll validators
59+
where
60+
dashOrDot = ['-', '.']
61+
validateAll vs x = mapM_ (validateWith x) vs >> return x
62+
validateWith x (p, err)
63+
| p x = Right x
64+
| otherwise = Left (err x)
65+
validChar c = isAscii c && (isLower c || isDigit c || c `elem` dashOrDot)
66+
validators =
67+
[ (not . T.null, const NotEmpty)
68+
, (T.all validChar, InvalidChars . T.unpack . T.filter (not . validChar))
69+
, (firstChar (`notElem` dashOrDot), const MustNotBeginSeparator)
70+
, (lastChar (`notElem` dashOrDot), const MustNotEndSeparator)
71+
, (not . T.isInfixOf "--", const RepeatedSeparators)
72+
, (not . T.isInfixOf "..", const RepeatedSeparators)
73+
, (T.length >>> (<= 50), TooLong . T.length)
74+
]
75+
firstChar p str = not (T.null str) && p (T.index str 0)
76+
lastChar p = firstChar p . T.reverse
77+
78+
runPackageName :: PackageName -> Text
79+
runPackageName (PackageName t) = t
80+
81+
preludePackageName :: PackageName
82+
preludePackageName = PackageName "prelude"
83+
84+
untitledPackageName :: PackageName
85+
untitledPackageName = PackageName "untitled"

psc-package.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ executable psc-package
2727
turtle ==1.3.*
2828
main-is: Main.hs
2929
other-modules: Paths_psc_package
30+
Types
3031
buildable: True
3132
hs-source-dirs: app
3233
ghc-options: -Wall -O2

0 commit comments

Comments
 (0)