Skip to content

Make spago install work when more advanced Dhall expressions are used #815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 131 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
bdf7f60
Check if pkgs are in set before doing other work
JordanMartinez Aug 28, 2021
c341413
Still install deps when `binding # listLit` used
JordanMartinez Aug 28, 2021
cb4eee4
Prep commit: convert some tests into suite
JordanMartinez Aug 29, 2021
765174e
Only install if have packages to install
JordanMartinez Aug 29, 2021
dd0257c
Test: add deps with ListAppend expr works
JordanMartinez Aug 29, 2021
7642ba8
Test: add deps no op when ListAppend used
JordanMartinez Aug 29, 2021
e5d87f0
Add changelog entry
JordanMartinez Aug 29, 2021
2afe15c
Move where code into same block as other one
JordanMartinez Aug 29, 2021
9373606
Move `case Dhall.Map.lookup "dependencies" down
JordanMartinez Aug 29, 2021
69867e6
Push RecortLit down to case statement
JordanMartinez Aug 29, 2021
cf4c9b0
Refactor ListAppend creation code to function
JordanMartinez Aug 29, 2021
1003c2e
Add docs to functions in the where clause
JordanMartinez Aug 29, 2021
5f02394
Rename 'r' to 'expr'
JordanMartinez Aug 29, 2021
122d9f7
Warn when dependencies already installed
JordanMartinez Aug 29, 2021
1ea5583
Move RecordLit case to where clause
JordanMartinez Aug 29, 2021
fe53318
Add Let case to get installed pkgs of bindings
JordanMartinez Aug 29, 2021
c3ac35a
Return original expr if update failed
JordanMartinez Aug 29, 2021
04963b6
Copy file to correct file name
JordanMartinez Aug 29, 2021
20b0445
Fix indentation of success file
JordanMartinez Aug 31, 2021
d28f446
Implement non-binding AST handling
JordanMartinez Aug 31, 2021
82eb8ee
Implement let-binding AST handling
JordanMartinez Sep 1, 2021
28924f2
Modify AST if encounter Embed
JordanMartinez Sep 1, 2021
b1455b5
Port config parsing to `parseConfig'`
JordanMartinez Sep 1, 2021
de47b2e
Verify that expr is a valid Config Dhall expr
JordanMartinez Sep 1, 2021
2a3e0fd
Update tests: verify Prefer Embed install works
JordanMartinez Sep 1, 2021
cb9d377
Rewrite AST modifier: use levels & update results
JordanMartinez Sep 2, 2021
feb5f68
Update tests to account for new output
JordanMartinez Sep 2, 2021
9b320d9
In root Prefer, merge update into RecordLit
JordanMartinez Sep 2, 2021
04a8372
Update changelog entry
JordanMartinez Sep 2, 2021
640cc13
Move nubSeq closer to its usage
JordanMartinez Sep 2, 2021
1e5b931
With: Add missing recordExpr case
JordanMartinez Sep 3, 2021
92dcee0
With: account for key stack field check
JordanMartinez Sep 3, 2021
e8f245c
Fix whitespace
JordanMartinez Sep 3, 2021
3e65860
ListAppend: simplify withinDeps case
JordanMartinez Sep 3, 2021
0a506c1
ListLit: don't drop potential type annotation
JordanMartinez Sep 3, 2021
1e3e420
RecordLit: only change to withinDeps if empty keys
JordanMartinez Sep 3, 2021
0b4f743
Test: add deps in advanced config expression
JordanMartinez Sep 3, 2021
7bb738f
With: fix level bug; switch to NonEmpty Text
JordanMartinez Sep 3, 2021
38023f5
Reduce chance of spelling mistakes
JordanMartinez Sep 3, 2021
fbeef11
Rename ExprLevel cases: exclude dependencies name
JordanMartinez Sep 3, 2021
27ff835
Make AtRootExpression indicate the field to update
JordanMartinez Sep 3, 2021
ae311bb
Convert args into sum type
JordanMartinez Sep 3, 2021
e9266e3
Rename addRawDeps to modifyRawAST
JordanMartinez Sep 3, 2021
b9de2ae
Expose pkgsToInstall via case statement
JordanMartinez Sep 3, 2021
2596f4c
Remove dependenciesText/nonEmptyDependencies
JordanMartinez Sep 3, 2021
b16f2c3
Rename update* to updateListText*
JordanMartinez Sep 3, 2021
e60f6f1
Generalize findInstalledPackages
JordanMartinez Sep 3, 2021
0411d80
Decouple update request from update data
JordanMartinez Sep 3, 2021
a6d40bf
Support addSources in raw AST modification
JordanMartinez Sep 3, 2021
41c350a
Support setting the name key
JordanMartinez Sep 3, 2021
78c7adc
Use id instead of \x -> x
JordanMartinez Sep 3, 2021
df5d7f8
Add TextLit case for SetText
JordanMartinez Sep 3, 2021
b7b9d69
Revert addRawDeps to original but cleaned code
JordanMartinez Sep 4, 2021
ecc3c98
Remove comment about resolved expression
JordanMartinez Sep 4, 2021
c83ee04
Add more docs on what the update is doing
JordanMartinez Sep 4, 2021
3ba6e7e
Add example and fix logic bug
JordanMartinez Sep 4, 2021
8808ec5
Fix last bug and test
JordanMartinez Sep 4, 2021
50f57f4
Account for de Brujin indices
JordanMartinez Sep 5, 2021
14ef1dc
Don't update binding if within field
JordanMartinez Sep 5, 2021
6f09812
Rename to ConfigModification and add docs
JordanMartinez Sep 6, 2021
e29a74f
Make AstUpdate further distinguished from ConfigModification
JordanMartinez Sep 6, 2021
b484dc4
Update docs for modifyRawAST
JordanMartinez Sep 6, 2021
21cb239
Update updateListTextByAppending docs
JordanMartinez Sep 6, 2021
d97882d
Make example span multiple lines for readability
JordanMartinez Sep 6, 2021
b19769e
Miscellaneous doc updates / clarifications
JordanMartinez Sep 6, 2021
ecd043c
Use correct syntax for haddocks
JordanMartinez Sep 6, 2021
ce6364b
Add case for each UpdateResult (even if redundant)
JordanMartinez Sep 6, 2021
e35b31f
Rename all `mbX` to `maybeX`
JordanMartinez Sep 6, 2021
6c207eb
Use clearer names for `modifyRawAST` functions
JordanMartinez Sep 6, 2021
58f1ebb
Update changelog: list as feature, not bug fix
JordanMartinez Sep 6, 2021
25fec51
Drop 'qw' and '-insane' prefix
JordanMartinez Sep 6, 2021
e5f3cf0
Drop header comments in advanced install fixtures
JordanMartinez Sep 6, 2021
11ade89
Fix parseConfig & parseConfig' (rename & add docs)
JordanMartinez Sep 6, 2021
c3d2640
Use newtype to store same expr in 2 diff versions
JordanMartinez Sep 6, 2021
ae73e14
Make `notInPackageSet` a top-level memer
JordanMartinez Sep 6, 2021
1fce4a3
Move nubSeq into Prelude
JordanMartinez Sep 6, 2021
85220e4
Use Dhall.toTextLit
JordanMartinez Sep 6, 2021
d945671
Fix another typo
JordanMartinez Sep 6, 2021
a8a1912
Rename `mpRight` (typo) to `maybeRight`
JordanMartinez Sep 6, 2021
f04dc5c
Verify: AST modify results in expected value
JordanMartinez Sep 7, 2021
b34125d
Convert logWarn to logDebug
JordanMartinez Sep 7, 2021
374e788
Derive Eq instance for Config
JordanMartinez Sep 8, 2021
0c73c31
Fix expectedConfig value
JordanMartinez Sep 8, 2021
3b4d259
Test `spago install` for all cases at root level
JordanMartinez Sep 8, 2021
c43da91
Update recordExpr, not update, on root With left
JordanMartinez Sep 8, 2021
fc6e288
Group simple root-level expressions
JordanMartinez Sep 8, 2021
66e2240
Add complex Field root-level expression tests
JordanMartinez Sep 8, 2021
4b2cf4d
Use recordExpr and select field before root key
JordanMartinez Sep 8, 2021
2436bb7
Fix setText bug
JordanMartinez Sep 8, 2021
7f49702
Sort parsed lists to ensure equality check passes
JordanMartinez Sep 8, 2021
8696c33
Use semantic equivalent check for config values
JordanMartinez Sep 8, 2021
a5d3df4
Replace wildcard with type
JordanMartinez Sep 8, 2021
b95b7fc
Uncomment stdout/stderr printing
JordanMartinez Sep 8, 2021
5edbb6a
Remove ability to add sources / set name
JordanMartinez Sep 8, 2021
54f239e
Final pubConfig case check is not always Left Left
JordanMartinez Sep 8, 2021
d382c5e
Make non-config dhall expression import available
JordanMartinez Sep 9, 2021
2d61419
Add debuggers to see control flow of updateExpr
JordanMartinez Sep 9, 2021
56237b4
Add test for `let x = { k = configValue } in x.k`
JordanMartinez Sep 9, 2021
0855fc7
Format test to it succeeds
JordanMartinez Sep 9, 2021
6a781f2
Account for variable's keyStack in binding updates
JordanMartinez Sep 9, 2021
9540d6f
Wrap embed in more local let binding
JordanMartinez Sep 9, 2021
3ec2d0e
Wrap local list text embed in list append
JordanMartinez Sep 9, 2021
a198a13
Remove unused binding in pattern match
JordanMartinez Sep 9, 2021
4741bd1
Completely drop the EncounteredEmbed
JordanMartinez Sep 9, 2021
80c6be5
Update success result's formatting
JordanMartinez Sep 9, 2021
7be8bbf
Port docs from EncounteredEmbed to updateExpr
JordanMartinez Sep 9, 2021
b3f9c9d
Add test for Field's WithinField level
JordanMartinez Sep 9, 2021
ee50a46
Drop 'qw' used to only run that test
JordanMartinez Sep 9, 2021
2f7a022
Test searching for field Prefer case
JordanMartinez Sep 9, 2021
91637a9
Add test: With at SearchingForField with many keys
JordanMartinez Sep 9, 2021
28f1cfd
Add test: Let at WithinField
JordanMartinez Sep 9, 2021
e3b5d72
Add skip1 and skip2 variants
JordanMartinez Sep 9, 2021
e8e06c3
Reformat test, so it passes
JordanMartinez Sep 9, 2021
88b9271
Make AtRootExpression's arg strict
JordanMartinez Sep 10, 2021
52bf5b2
Enable WithinField to track let bindings
JordanMartinez Sep 10, 2021
b094ef2
Ensure update always occurs within outermost let binding in WithinField
JordanMartinez Sep 10, 2021
3c75672
VariableName second arg: `NonEmpty Text` -> `[Text]`
JordanMartinez Sep 10, 2021
6947a73
Optimize code: convert ExprLevel to a keyStack
JordanMartinez Sep 10, 2021
cd3a0d8
Fix bug: Wrap embed in listAppend if no keyStack
JordanMartinez Sep 10, 2021
ab25eaf
Support the Left version of Project
JordanMartinez Sep 11, 2021
e08f1ad
Update/add docs over each case
JordanMartinez Sep 12, 2021
aabc68b
Remove redundant first case for With
JordanMartinez Sep 12, 2021
6212952
Update docs for let binding
JordanMartinez Sep 12, 2021
ad88c39
Reduce duplication in debug loggers
JordanMartinez Sep 12, 2021
47a056e
Stop invalid upward updates; update Var immediately
JordanMartinez Sep 12, 2021
d62ecb9
Change log function back to logDebug
JordanMartinez Sep 12, 2021
507f72f
Clean up doc comments
JordanMartinez Sep 13, 2021
a8ea252
Rename astMod to configMod to match type
JordanMartinez Sep 13, 2021
5b4dd9a
Rename keyStack to levelKeyStack in doc comments
JordanMartinez Sep 13, 2021
accac7c
Don't call toList on List arg
JordanMartinez Sep 13, 2021
c38f94b
Match on VariableName case regardless of arg size
JordanMartinez Sep 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Features:
- Make `spago install` work on more advanced Dhall expressions (#815)

- Support `ListAppend`: `{ dependencies = list1 # [ "package"] }`
- Support `Prefer`: `let config = ... in config // { dependencies = config.dependencies # [ "foo" ] }`
- Support `With`: `let config = ... in config with dependencies = config.dependencies # [ "foo" ]`
- Support `Field`: `let multiConfig = ... in multiConfig.thisProjectConfig`
- Support `Project _ Left`: `{ name, packages, sources, dependencies = [], otherName }.{ name, packages, sources, dependencies }`
- Support `Let`: `let config = ... in config`
- Support `Embed`: `./spago.dhall`
- Support `Var`: `let deps = ... in { ..., dependencies = deps }`

Bugfixes:
- Don't warn on unused deps when building --deps-only. (#794)

Expand Down
1 change: 1 addition & 0 deletions spago.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ library
Spago.Command.Path
Spago.Command.Verify
Spago.Config
Spago.Config.AST
Spago.Dhall
Spago.DryRun
Spago.Env
Expand Down
274 changes: 219 additions & 55 deletions src/Spago/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import qualified Web.Bower.PackageMeta as Bower

import qualified Spago.Dhall as Dhall
import qualified Spago.Messages as Messages
import qualified Spago.Config.AST as AST
import qualified Spago.PackageSet as PackageSet
import qualified Spago.PscPackage as PscPackage
import qualified Spago.Templates as Templates
Expand Down Expand Up @@ -110,7 +111,8 @@ parsePackageSet pkgs = do
pure PackageSet{..}


-- | Tries to read in a Spago Config
-- | Tries to parse the raw Dhall expression stored
-- in the @./spago.dhall@ file into a `Config` value.
parseConfig
:: (HasLogFunc env, HasConfigPath env)
=> RIO env Config
Expand All @@ -120,30 +122,41 @@ parseConfig = do

ConfigPath path <- view (the @ConfigPath)
expr <- liftIO $ Dhall.inputExpr $ "./" <> path
case expr of
Dhall.RecordLit ks' -> do
let ks = Dhall.extractRecordValues ks'
let sourcesType = Dhall.list (Dhall.auto :: Dhall.Decoder SourcePath)
name <- Dhall.requireTypedKey ks "name" Dhall.strictText
dependencies <- Dhall.requireTypedKey ks "dependencies" dependenciesType
configSourcePaths <- Dhall.requireTypedKey ks "sources" sourcesType
alternateBackend <- Dhall.maybeTypedKey ks "backend" Dhall.strictText

let ensurePublishConfig = do
publishLicense <- Dhall.requireTypedKey ks "license" Dhall.strictText
publishRepository <- Dhall.requireTypedKey ks "repository" Dhall.strictText
pure PublishConfig{..}
publishConfig <- try ensurePublishConfig

packageSet <- Dhall.requireKey ks "packages" (\case
Dhall.RecordLit pkgs -> parsePackageSet (Dhall.extractRecordValues pkgs)
something -> throwM $ Dhall.PackagesIsNotRecord something)

pure Config{..}
_ -> case Dhall.TypeCheck.typeOf expr of
maybeConfig <- parseConfigNormalizedExpr expr
case maybeConfig of
Just config -> pure config
Nothing -> case Dhall.TypeCheck.typeOf expr of
Right e -> throwM $ Dhall.ConfigIsNotRecord e
Left err -> throwM err

-- |
-- Attempts to parse a normalized Dhall expression (i.e. all imports have been resolved)
-- into a `Config` value.
parseConfigNormalizedExpr
:: (HasLogFunc env)
=> ResolvedExpr -> RIO env (Maybe Config)
parseConfigNormalizedExpr = \case
Dhall.RecordLit ks' -> do
let ks = Dhall.extractRecordValues ks'
let sourcesType = Dhall.list (Dhall.auto :: Dhall.Decoder SourcePath)
name <- Dhall.requireTypedKey ks "name" Dhall.strictText
dependencies <- List.sort <$> Dhall.requireTypedKey ks "dependencies" dependenciesType
configSourcePaths <- List.sort <$> Dhall.requireTypedKey ks "sources" sourcesType
alternateBackend <- Dhall.maybeTypedKey ks "backend" Dhall.strictText

let ensurePublishConfig = do
publishLicense <- Dhall.requireTypedKey ks "license" Dhall.strictText
publishRepository <- Dhall.requireTypedKey ks "repository" Dhall.strictText
pure PublishConfig{..}
publishConfig <- try ensurePublishConfig

packageSet <- Dhall.requireKey ks "packages" (\case
Dhall.RecordLit pkgs -> parsePackageSet (Dhall.extractRecordValues pkgs)
something -> throwM $ Dhall.PackagesIsNotRecord something)

pure $ Just Config{..}
_ ->
pure Nothing

-- | Checks that the Spago config is there and readable
ensureConfig
Expand Down Expand Up @@ -321,36 +334,30 @@ updateName newName (Dhall.RecordLit kvs)
updateName _ other = other

addRawDeps :: HasLogFunc env => Config -> [PackageName] -> Expr -> RIO env Expr
addRawDeps config newPackages r@(Dhall.RecordLit kvs) = case Dhall.Map.lookup "dependencies" kvs of
Just (Dhall.RecordField { recordFieldValue = Dhall.ListLit _ dependencies }) -> do
case NonEmpty.nonEmpty notInPackageSet of
-- If none of the newPackages are outside of the set, add them to existing dependencies
Nothing -> do
oldPackages <- traverse (throws . Dhall.fromTextLit) dependencies
let newDepsExpr
= Dhall.makeRecordField
$ Dhall.ListLit Nothing $ fmap (Dhall.toTextLit . packageName)
$ Seq.sort $ nubSeq (Seq.fromList newPackages <> fmap PackageName oldPackages)
pure $ Dhall.RecordLit $ Dhall.Map.insert "dependencies" newDepsExpr kvs
Just pkgs -> do
logWarn $ display $ Messages.failedToAddDeps $ NonEmpty.map packageName pkgs
pure r
where
Config { packageSet = PackageSet{..} } = config
notInPackageSet = filter (\p -> Map.notMember p packagesDB) newPackages

-- | Code from https://stackoverflow.com/questions/45757839
nubSeq :: Ord a => Seq a -> Seq a
nubSeq xs = (fmap fst . Seq.filter (uncurry notElem)) (Seq.zip xs seens)
where
seens = Seq.scanl (flip Set.insert) Set.empty xs
Just _ -> do
logWarn "Failed to add dependencies. The `dependencies` field wasn't a List of Strings."
pure r
Nothing -> do
logWarn "Failed to add dependencies. You should have a record with the `dependencies` key for this to work."
pure r
addRawDeps _ _ other = pure other
addRawDeps config newPackages expr =
case notInPackageSet config newPackages of
Just pkgs -> do
logWarn $ display $ Messages.failedToAddDeps $ NonEmpty.map packageName pkgs
pure expr
-- If none of the newPackages are outside of the set, add them to existing dependencies
Nothing -> case expr of
r@(Dhall.RecordLit kvs) ->
case Dhall.Map.lookup "dependencies" kvs of
Just Dhall.RecordField { recordFieldValue = Dhall.ListLit _ dependencies } -> do
oldPackages <- traverse (throws . Dhall.fromTextLit) dependencies
let newDepsExpr
= Dhall.makeRecordField
$ Dhall.ListLit Nothing $ fmap (Dhall.toTextLit . packageName)
$ Seq.sort $ nubSeq (Seq.fromList newPackages <> fmap PackageName oldPackages)
pure $ Dhall.RecordLit $ Dhall.Map.insert "dependencies" newDepsExpr kvs
Just _ -> do
logWarn "Failed to add dependencies. The `dependencies` field wasn't a List of Strings."
pure r
Nothing -> do
logWarn "Failed to add dependencies. You should have a record with the `dependencies` key for this to work."
pure r
_ ->
pure expr

addSourcePaths :: Expr -> Expr
addSourcePaths (Dhall.RecordLit kvs)
Expand Down Expand Up @@ -398,6 +405,30 @@ withConfigAST transform = do
else logDebug "Transformed config is the same as the read one, not overwriting it"
pure exprHasChanged

-- | Takes a function that manipulates the Dhall AST of the Config, and tries to run it
-- on the current config. If it succeeds, it writes back to file the result returned.
withRawConfigAST
:: (HasLogFunc env, HasConfigPath env)
=> (AST.ResolvedUnresolvedExpr -> RIO env Expr) -> RIO env Bool
withRawConfigAST transform = do
ConfigPath path <- view (the @ConfigPath)
rawConfig <- liftIO $ Dhall.readRawExpr path
normalizedExpr <- liftIO $ Dhall.inputExpr $ "./" <> path
case rawConfig of
Nothing -> die [ display $ Messages.cannotFindConfig path ]
Just (header, expr) -> do
let
unresolved = Dhall.Core.denote expr
resolved = normalizedExpr

newExpr <- transform $ AST.ResolvedUnresolvedExpr (resolved, unresolved)
-- Write the new expression only if it has actually changed
let exprHasChanged = Dhall.Core.denote expr /= newExpr
if exprHasChanged
then liftIO $ Dhall.writeRawExpr path (header, newExpr)
else logDebug "Transformed config is the same as the read one, not overwriting it"
pure exprHasChanged


transformMExpr
:: MonadIO m
Expand All @@ -416,10 +447,143 @@ transformMExpr rules =
-- If everything is fine instead, it will add the new deps, sort all the
-- dependencies, and write the Config back to file.
addDependencies
:: (HasLogFunc env, HasConfigPath env)
:: forall env
. (HasLogFunc env, HasConfigPath env)
=> Config -> [PackageName]
-> RIO env ()
addDependencies config newPackages = do
configHasChanged <- withConfigAST $ addRawDeps config newPackages
addDependencies config@Config { dependencies = deps, publishConfig = pubConfig } newPackages = do
configHasChanged <- case notInPackageSet config newPackages of
Just pkgsNotInPackageSet -> do
logWarn $ display $ Messages.failedToAddDeps $ NonEmpty.map packageName pkgsNotInPackageSet
pure False
Nothing -> do
let
expectedConfig :: Config
expectedConfig = config { dependencies = mkExpectedConfigDeps, publishConfig = mkExpectedPubConifg }
withRawConfigAST $ \sameExpr -> do
newExpr <- AST.modifyRawConfigExpression (AST.AddPackages newPackages) sameExpr
-- Verify that returned expression produces the expected `Config` value if parsed
-- before we return it.
normalizedExpr <- liftIO $ Dhall.inputExpr $ pretty newExpr
maybeResult <- parseConfigNormalizedExpr normalizedExpr `catch` (\(_ :: SomeException) -> pure Nothing)
case maybeResult of
Just parsedConfig -> do
validModification <- expectedConfig `isSemanticallyEquivalentTo` parsedConfig
if validModification then do
pure newExpr
else do
logWarn "Failed to add dependencies."
logDebug $
"Raw AST modification did not produce the expected Dhall expression. " <>
"If parsed in a future command, the AST would not produce the expected `Config` value."
pure $ snd $ AST.resolvedUnresolvedExpr sameExpr
Nothing -> do
logWarn "Failed to add dependencies."
logDebug "Raw AST modification did not produce a valid `spago.dhall` file."
pure $ snd $ AST.resolvedUnresolvedExpr sameExpr

unless configHasChanged $
logWarn "Configuration file was not updated."

where
mkExpectedConfigDeps = List.nub $ List.sort $ deps <> newPackages

-- |
-- If the @pubConfig@ parsing fails, it will fail on the first key checked (i.e. the @license@ key).
-- When it does fail, it records a map of the expression and that map does not include the new packages
-- When the modified expression is parsed, it will also fail at the @license@ key. However, it\'s
-- map will include the new packages.
--
-- Thus, we need to update the map in the expected config, so the equality check will pass.
mkExpectedPubConifg = case pubConfig of
Left (Dhall.RequiredKeyMissing key kvs) ->
Left (Dhall.RequiredKeyMissing key newKvs)
where
newKvs = Dhall.Map.insertWith insertNewPackages "dependencies" newPackagesExpr kvs

newPackagesExpr :: ResolvedExpr
newPackagesExpr = Dhall.ListLit Nothing $ Seq.fromList $ fmap (Dhall.toTextLit . packageName) newPackages

insertNewPackages :: ResolvedExpr -> ResolvedExpr -> ResolvedExpr
insertNewPackages (Dhall.ListLit an left) (Dhall.ListLit _ right) =
Dhall.ListLit an $ nubSeq $ left <> right
insertNewPackages other _ = other

x -> x

-- |
-- Unfortunately, we cannot just check whether the expected config is equal to the actual config
-- because "Dhall.Map.Map" keeps track of order when equating two maps.
-- For some cases, this \"values are only equal if ordered the same\" check will cause a failure when
-- we attempt to parse the @PublishConfig@ and fail. In such circumstances, the failure
-- message will be @Left (Dhall.RequiredKeyMissing licenseOrRepositoryText map)@ and @map@ will have a different
-- order in the expected config than it will in the parsed config.
--
-- Moreover, if the config equality check below fails, it is more helpful to understand what parts of
-- the @Config@ values were considered unequal. Thus, besides doing a typical @expected == actual@ check,
-- we will log debug messages to the console while checking all values in case there are multiple
-- values that are different.
isSemanticallyEquivalentTo :: Config -> Config -> RIO env Bool
isSemanticallyEquivalentTo
Config { name = expN, dependencies = expD, packageSet = expPS, alternateBackend = expAB, configSourcePaths = expCSP, publishConfig = expPC }
Config { name = actN, dependencies = actD, packageSet = actPS, alternateBackend = actAB, configSourcePaths = actCSP, publishConfig = actPC }
= checkAll
[ checkValue expN actN "Config: name"
, checkValue expD actD "Config: dependencies"
, checkValue expPS actPS "Config: package set"
, checkValue expAB actAB "Config: alternate backend"
, checkValue expCSP actCSP "Config: config source paths"
, checkPC expPC actPC
]

checkAll :: [RIO env Bool] -> RIO env Bool
checkAll = foldl' (\acc n -> do
prev <- acc
next <- n
pure $ prev && next) (pure True)

checkValue :: forall a. Eq a => Show a => a -> a -> Utf8Builder -> RIO env Bool
checkValue expected actual msg
| expected == actual = do
logDebug $ msg <> " - No problem here."
pure True
| otherwise = do
logDebug $ msg <> " - Found mismatch"
logDebug $ displayShow expected
logDebug $ displayShow actual
pure False

checkPC :: Either (Dhall.ReadError Void) PublishConfig -> Either (Dhall.ReadError Void) PublishConfig -> RIO env Bool
checkPC (Right l) (Right r) = do
checkValue l r "Config: pubConfig - Right"
checkPC (Left (Dhall.RequiredKeyMissing k1 kvs1)) (Left (Dhall.RequiredKeyMissing k2 kvs2)) = do
checkAll
[ checkValue k1 k2 "Config: pubConfig - Left RequiredKeyMissing: keys"
, checkValue (sortDependencies kvs1) (sortDependencies kvs2) "Config: pubConfig - Left RequiredKeyMissing: maps"
]
checkPC l r = do
logDebug "Config: pubConfig: unexpected value in both"
logDebug $ "Expected value: " <> displayShow l
logDebug $ "Actual value: " <> displayShow r
pure False

sortDependencies :: Dhall.Map.Map Text ResolvedExpr -> Dhall.Map.Map Text ResolvedExpr
sortDependencies x = case Dhall.Map.lookup dependenciesText x of
Just (Dhall.ListLit a pkgs) ->
Dhall.Map.insert dependenciesText (Dhall.ListLit a (Seq.sortOn toText pkgs)) x
_ ->
x
where
dependenciesText = "dependencies"

toText = \case
Dhall.TextLit (Dhall.Chunks [] t) -> t
_ -> error "impossible: A normalized expression that produced a valid `Config` value should only have a `TextLit` here"

-- |
-- Returns a non-empty list of packages not found in the package set
-- or @Nothing@ if all are found in the package set.
notInPackageSet
:: Config -> [PackageName] -> Maybe (NonEmpty PackageName)
notInPackageSet Config { packageSet = PackageSet{..} } newPackages =
NonEmpty.nonEmpty $ filter (\p -> Map.notMember p packagesDB) newPackages
Loading