nix-diff-1.0.21: src/Nix/Diff/Types.hs
-- Needed for `deriving Arbitrary via GenericArbitrary DerivationDiff`:
{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
-- Needed for `deriving instance Data (DerivationOutput StorePath Text)`:
{-# OPTIONS_GHC -Wno-orphans #-}
module Nix.Diff.Types where
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import Data.Text (Text)
import Nix.Derivation (DerivationOutput (..))
import Nix.Diff.Store ( StorePath )
import qualified Patience
import GHC.Generics (Generic)
import Data.Foldable
import Data.Aeson
import Data.Aeson.Types
import Test.QuickCheck
import Test.QuickCheck.Instances ()
import Test.QuickCheck.Arbitrary.Generic (GenericArbitrary(..))
import Data.Data (Data)
{- ** NOTE: Lawless instances
All the Arbitrary instances here are written to
check if `decode . encode == id` rule was broken,
so they don't respect to internal laws of these types,
such as "Maps from `extraOutputs` must not have
intersecting keys" and so on.
If you want to test these invariants, you have to rewrite
instances manually.
-}
data Changed a = Changed { before :: a, now :: a }
deriving stock (Eq, Show, Functor, Foldable, Traversable, Generic, Data)
instance (Arbitrary a) => Arbitrary (Changed a) where
arbitrary = Changed <$> arbitrary <*> arbitrary
shrink = genericShrink
instance ToJSON a => ToJSON (Changed a) where
toJSON = changedToJSON toJSON
instance FromJSON a => FromJSON (Changed a) where
parseJSON = changedFromJSON parseJSON
newtype TextDiff = TextDiff {unTextDiff :: [Patience.Item Text]}
deriving stock (Eq, Show, Data)
instance Arbitrary TextDiff where
arbitrary = TextDiff <$> listOf arbitraryItem
instance ToJSON TextDiff where
toJSON textDiff = listValue itemToJSON textDiff.unTextDiff
instance FromJSON TextDiff where
parseJSON v = TextDiff <$> (traverse itemFromJSON =<< parseJSON v)
-- Helpful aliases
type OutputHash = Text
type Platform = Text
type Builder = Text
type Argument = Text
-- | A set of Nix derivation output names.
newtype OutputNames = OutputNames {unOutputNames :: Set Text}
deriving newtype (Eq, Ord, ToJSON, FromJSON)
deriving stock (Show, Generic, Data)
deriving Arbitrary via GenericArbitrary OutputNames
-- Derivation diff
data DerivationDiff
= DerivationsAreTheSame
| AlreadyCompared
| OnlyAlreadyComparedBelow { outputStructure :: Changed OutputStructure}
| NamesDontMatch { outputStructure :: Changed OutputStructure}
| OutputsDontMatch { outputStructure :: Changed OutputStructure}
| DerivationDiff
{ outputStructure :: Changed OutputStructure
, outputsDiff :: OutputsDiff
, platformDiff :: Maybe (Changed Platform)
-- ^ Will be Nothing, if Platform does not change
, builderDiff :: Maybe (Changed Builder)
-- ^ Will be Nothing, if Builder does not change
, argumentsDiff :: Maybe ArgumentsDiff
-- ^ Will be Nothing, if arguments are equal
, sourcesDiff :: SourcesDiff
, inputsDiff :: InputsDiff
, envDiff :: Maybe EnvironmentDiff
-- ^ Will be Nothing, if environment comparison is skipped
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary DerivationDiff
-- Output structure
data OutputStructure = OutputStructure
{ derivationPath :: StorePath
, derivationOutputs :: OutputNames
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary OutputStructure
-- ** Outputs diff
data OutputsDiff = OutputsDiff
{ extraOutputs :: Maybe (Changed (Map Text (DerivationOutput StorePath Text)))
-- ^ Map from derivation name to its outputs.
-- Will be Nothing, if `Data.Map.difference` gives
-- empty Maps for both new and old outputs
, outputHashDiff :: [OutputDiff]
-- ^ Difference of outputs with the same name.
-- Will be empty, if all outputs are equal.
}
deriving stock (Eq, Show, Data)
deriving instance Data (DerivationOutput StorePath Text)
instance Arbitrary OutputsDiff where
arbitrary = OutputsDiff <$> arbitraryExtraOutputs <*> arbitrary
where
arbitraryExtraOutputs =
oneof [pure Nothing, Just <$> arbitraryChangedMap]
arbitraryChangedMap =
Changed <$> arbitraryMap <*> arbitraryMap
arbitraryMap = Map.fromList <$>
listOf ((,) <$> arbitrary <*> arbitraryDerivationOutput)
instance ToJSON OutputsDiff where
toJSON OutputsDiff{..} = object
[ "extraOutputs" .= fmap (changedToJSON extraOutputsToJSON) extraOutputs
, "outputHashDiff" .= outputHashDiff
]
where
extraOutputsToJSON :: Map Text (DerivationOutput StorePath Text) -> Value
extraOutputsToJSON = toJSON . fmap derivationOutputToJSON
derivationOutputToJSON :: DerivationOutput StorePath Text -> Value
derivationOutputToJSON DerivationOutput{..} = object
[ "path" .= path
, "hashAlgo" .= hashAlgo
, "hash" .= hash
]
instance FromJSON OutputsDiff where
parseJSON = withObject "OutputsDiff" \o -> do
extraOutputsWithoutParsingDerivationOutputs <- o .: "extraOutputs"
ohd <- o .: "outputHashDiff"
eo <- (traverse . traverse . traverse)
derivationOutputFromJSON
extraOutputsWithoutParsingDerivationOutputs
pure $ OutputsDiff eo ohd
where
derivationOutputFromJSON :: Value -> Parser (DerivationOutput StorePath Text)
derivationOutputFromJSON = withObject "DerivationOutput" \o ->
DerivationOutput <$> o .: "path" <*> o .: "hashAlgo" <*> o .: "hash"
data OutputDiff = OutputDiff
{ outputName :: Text
, hashDifference :: Changed OutputHash
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary OutputDiff
-- ** Arguments diff
newtype ArgumentsDiff = ArgumentsDiff
{ unArgumetsDiff :: NonEmpty (Patience.Item Argument)
}
deriving stock (Eq, Show, Data)
instance Arbitrary ArgumentsDiff where
arbitrary = ArgumentsDiff . NonEmpty.fromList <$> listOf1 arbitraryItem
instance ToJSON ArgumentsDiff where
toJSON argumentsDiff = listValue itemToJSON (toList argumentsDiff.unArgumetsDiff)
instance FromJSON ArgumentsDiff where
parseJSON v = ArgumentsDiff <$> (traverse itemFromJSON =<< parseJSON v)
-- ** Sources diff
data SourcesDiff = SourcesDiff
{ extraSrcNames :: Maybe (Changed (Set Text))
-- ^ Will be Nothing, if there is no extra source names
, srcFilesDiff :: [SourceFileDiff]
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary SourcesDiff
data SourceFileDiff
= OneSourceFileDiff
{ srcName :: Text
, srcContentDiff :: Maybe TextDiff
-- ^ Will be Nothing, if any of source files not exists
}
| SomeSourceFileDiff
{ srcName :: Text
, srcFileDiff :: Changed [StorePath]
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary SourceFileDiff
-- ** Inputs diff
data InputsDiff = InputsDiff
{ inputExtraNames :: Maybe (Changed (Set Text))
-- ^ Will be Nothing, if there is no extra input names
, inputDerivationDiffs :: [InputDerivationsDiff]
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary InputsDiff
data InputDerivationsDiff
= OneDerivationDiff
{ drvName :: Text
, drvDiff :: DerivationDiff
}
| SomeDerivationsDiff
{ drvName :: Text
, extraPartsDiff :: Changed (Map StorePath OutputNames)
}
| -- | Many input derivations differ, but they've all already been compared.
ManyDerivationsAlreadyComparedDiff
{ drvNames :: Set Text
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary InputDerivationsDiff
-- ** Environment diff
data EnvironmentDiff
= EnvironmentsAreEqual
| EnvironmentDiff
{ extraEnvDiff :: Changed (Map Text Text)
, envContentDiff :: [EnvVarDiff]
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary EnvironmentDiff
data EnvVarDiff = EnvVarDiff
{ envKey :: Text
, envValueDiff :: TextDiff
}
deriving stock (Eq, Show, Generic, Data)
deriving anyclass (ToJSON, FromJSON)
deriving Arbitrary via GenericArbitrary EnvVarDiff
-- ** Helpers
changedToJSON :: (a -> Value) -> Changed a -> Value
changedToJSON converter Changed{..} = object
[ "before" .= converter before
, "now" .= converter now
]
changedFromJSON :: (Value -> Parser a) -> Value -> Parser (Changed a)
changedFromJSON innerParser = withObject "Changed" \o -> do
b <- o .: "before"
n <- o .: "now"
Changed <$> innerParser b <*> innerParser n
itemToJSON :: ToJSON v => Patience.Item v -> Value
itemToJSON (Patience.Old a) = object
[ "type" .= ("Old" :: Text), "content" .= a]
itemToJSON (Patience.New a) = object
[ "type" .= ("New" :: Text), "content" .= a]
itemToJSON (Patience.Both a _) = object
[ "type" .= ("Both" :: Text), "content" .= a]
itemFromJSON :: FromJSON v => Value -> Parser (Patience.Item v)
itemFromJSON = withObject "Item" \o -> do
t <- o .: "type"
c <- o .: "content"
case t :: Text of
"Old" -> pure $ Patience.Old c
"New" -> pure $ Patience.New c
"Both" -> pure $ Patience.Both c c
_ -> fail "Item: unexpected type"
arbitraryItem :: Arbitrary a => Gen (Patience.Item a)
arbitraryItem = do
t <- arbitrary
ctr <- elements [old, new, both]
pure (ctr t)
where
old = Patience.Old
new = Patience.New
both x = Patience.Both x x
arbitraryDerivationOutput :: (Arbitrary fp, Arbitrary txt) => Gen (DerivationOutput fp txt)
arbitraryDerivationOutput = DerivationOutput <$> arbitrary <*> arbitrary <*> arbitrary