module Conformance.Rows
( Row (..)
, RowState (..)
, ShownState (..)
, expectedRowCount
, ownedDenominator
, loadRows
, validateInventory
, effectiveState
, renderInventory
, renderRow
) where
import Data.Aeson
( FromJSON (..)
, eitherDecode
, withObject
, withText
, (.:)
, (.:?)
)
import Data.ByteString.Lazy qualified as BSL
import Data.List (nub, sort)
import Data.Text (Text)
import Data.Text qualified as T
import Conformance.Receipt (Receipt (..), Verdict (..))
expectedRowCount :: Int
expectedRowCount :: Int
expectedRowCount = Int
48
ownedDenominator :: Int
ownedDenominator :: Int
ownedDenominator = Int
47
data RowState
= Uncovered
| BoundElsewhere
| OutOfScope
deriving stock (Int -> RowState -> ShowS
[RowState] -> ShowS
RowState -> String
(Int -> RowState -> ShowS)
-> (RowState -> String) -> ([RowState] -> ShowS) -> Show RowState
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> RowState -> ShowS
showsPrec :: Int -> RowState -> ShowS
$cshow :: RowState -> String
show :: RowState -> String
$cshowList :: [RowState] -> ShowS
showList :: [RowState] -> ShowS
Show, RowState -> RowState -> Bool
(RowState -> RowState -> Bool)
-> (RowState -> RowState -> Bool) -> Eq RowState
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: RowState -> RowState -> Bool
== :: RowState -> RowState -> Bool
$c/= :: RowState -> RowState -> Bool
/= :: RowState -> RowState -> Bool
Eq, Eq RowState
Eq RowState =>
(RowState -> RowState -> Ordering)
-> (RowState -> RowState -> Bool)
-> (RowState -> RowState -> Bool)
-> (RowState -> RowState -> Bool)
-> (RowState -> RowState -> Bool)
-> (RowState -> RowState -> RowState)
-> (RowState -> RowState -> RowState)
-> Ord RowState
RowState -> RowState -> Bool
RowState -> RowState -> Ordering
RowState -> RowState -> RowState
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: RowState -> RowState -> Ordering
compare :: RowState -> RowState -> Ordering
$c< :: RowState -> RowState -> Bool
< :: RowState -> RowState -> Bool
$c<= :: RowState -> RowState -> Bool
<= :: RowState -> RowState -> Bool
$c> :: RowState -> RowState -> Bool
> :: RowState -> RowState -> Bool
$c>= :: RowState -> RowState -> Bool
>= :: RowState -> RowState -> Bool
$cmax :: RowState -> RowState -> RowState
max :: RowState -> RowState -> RowState
$cmin :: RowState -> RowState -> RowState
min :: RowState -> RowState -> RowState
Ord)
instance FromJSON RowState where
parseJSON :: Value -> Parser RowState
parseJSON = String -> (Text -> Parser RowState) -> Value -> Parser RowState
forall a. String -> (Text -> Parser a) -> Value -> Parser a
withText String
"RowState" ((Text -> Parser RowState) -> Value -> Parser RowState)
-> (Text -> Parser RowState) -> Value -> Parser RowState
forall a b. (a -> b) -> a -> b
$ \Text
t -> case Text
t of
Text
"uncovered" -> RowState -> Parser RowState
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure RowState
Uncovered
Text
"bound-elsewhere" -> RowState -> Parser RowState
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure RowState
BoundElsewhere
Text
"out-of-scope" -> RowState -> Parser RowState
forall a. a -> Parser a
forall (f :: * -> *) a. Applicative f => a -> f a
pure RowState
OutOfScope
Text
"executed" ->
String -> Parser RowState
forall a. String -> Parser a
forall (m :: * -> *) a. MonadFail m => String -> m a
fail
String
"rejected: executed is a receipt, not a field — \
\rows.json carries the plan; a run receipt establishes \
\execution"
Text
_ -> String -> Parser RowState
forall a. String -> Parser a
forall (m :: * -> *) a. MonadFail m => String -> m a
fail (String
"unknown row state: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
t)
data Row = Row
{ Row -> Text
rowId :: !Text
, Row -> Text
rowGroup :: !Text
, Row -> Text
rowRequirement :: !Text
, Row -> Text
rowSource :: !Text
, Row -> Text
rowExpected :: !Text
, Row -> RowState
rowState :: !RowState
, Row -> Maybe Text
rowNote :: !(Maybe Text)
, Row -> Maybe Text
rowEvidence :: !(Maybe Text)
}
deriving stock (Int -> Row -> ShowS
[Row] -> ShowS
Row -> String
(Int -> Row -> ShowS)
-> (Row -> String) -> ([Row] -> ShowS) -> Show Row
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Row -> ShowS
showsPrec :: Int -> Row -> ShowS
$cshow :: Row -> String
show :: Row -> String
$cshowList :: [Row] -> ShowS
showList :: [Row] -> ShowS
Show, Row -> Row -> Bool
(Row -> Row -> Bool) -> (Row -> Row -> Bool) -> Eq Row
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Row -> Row -> Bool
== :: Row -> Row -> Bool
$c/= :: Row -> Row -> Bool
/= :: Row -> Row -> Bool
Eq)
instance FromJSON Row where
parseJSON :: Value -> Parser Row
parseJSON = String -> (Object -> Parser Row) -> Value -> Parser Row
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"Row" ((Object -> Parser Row) -> Value -> Parser Row)
-> (Object -> Parser Row) -> Value -> Parser Row
forall a b. (a -> b) -> a -> b
$ \Object
o ->
Text
-> Text
-> Text
-> Text
-> Text
-> RowState
-> Maybe Text
-> Maybe Text
-> Row
Row
(Text
-> Text
-> Text
-> Text
-> Text
-> RowState
-> Maybe Text
-> Maybe Text
-> Row)
-> Parser Text
-> Parser
(Text
-> Text
-> Text
-> Text
-> RowState
-> Maybe Text
-> Maybe Text
-> Row)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"id"
Parser
(Text
-> Text
-> Text
-> Text
-> RowState
-> Maybe Text
-> Maybe Text
-> Row)
-> Parser Text
-> Parser
(Text
-> Text -> Text -> RowState -> Maybe Text -> Maybe Text -> Row)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"group"
Parser
(Text
-> Text -> Text -> RowState -> Maybe Text -> Maybe Text -> Row)
-> Parser Text
-> Parser
(Text -> Text -> RowState -> Maybe Text -> Maybe Text -> Row)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"requirement"
Parser
(Text -> Text -> RowState -> Maybe Text -> Maybe Text -> Row)
-> Parser Text
-> Parser (Text -> RowState -> Maybe Text -> Maybe Text -> Row)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"source"
Parser (Text -> RowState -> Maybe Text -> Maybe Text -> Row)
-> Parser Text
-> Parser (RowState -> Maybe Text -> Maybe Text -> Row)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser Text
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"expected"
Parser (RowState -> Maybe Text -> Maybe Text -> Row)
-> Parser RowState -> Parser (Maybe Text -> Maybe Text -> Row)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser RowState
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"state"
Parser (Maybe Text -> Maybe Text -> Row)
-> Parser (Maybe Text) -> Parser (Maybe Text -> Row)
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"note"
Parser (Maybe Text -> Row) -> Parser (Maybe Text) -> Parser Row
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Object
o Object -> Key -> Parser (Maybe Text)
forall a. FromJSON a => Object -> Key -> Parser (Maybe a)
.:? Key
"evidence"
newtype Inventory = Inventory {Inventory -> [Row]
inventoryRows :: [Row]}
instance FromJSON Inventory where
parseJSON :: Value -> Parser Inventory
parseJSON = String -> (Object -> Parser Inventory) -> Value -> Parser Inventory
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"Inventory" ((Object -> Parser Inventory) -> Value -> Parser Inventory)
-> (Object -> Parser Inventory) -> Value -> Parser Inventory
forall a b. (a -> b) -> a -> b
$ \Object
o ->
[Row] -> Inventory
Inventory ([Row] -> Inventory) -> Parser [Row] -> Parser Inventory
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
o Object -> Key -> Parser [Row]
forall a. FromJSON a => Object -> Key -> Parser a
.: Key
"rows"
loadRows :: FilePath -> IO (Either String [Row])
loadRows :: String -> IO (Either String [Row])
loadRows String
path = do
content <- String -> IO ByteString
BSL.readFile String
path
pure $ case eitherDecode content of
Left String
err -> String -> Either String [Row]
forall a b. a -> Either a b
Left (String
"rows.json does not parse: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
err)
Right Inventory
inv -> [Row] -> Either String [Row]
validateInventory (Inventory -> [Row]
inventoryRows Inventory
inv)
validateInventory :: [Row] -> Either String [Row]
validateInventory :: [Row] -> Either String [Row]
validateInventory [Row]
rows
| [Row] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Row]
rows = String -> Either String [Row]
forall a b. a -> Either a b
Left String
"inventory is empty"
| [Row] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Row]
rows Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= Int
expectedRowCount =
String -> Either String [Row]
forall a b. a -> Either a b
Left
( String
"inventory carries "
String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Int -> String
forall a. Show a => a -> String
show ([Row] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Row]
rows)
String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
" rows, expected "
String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Int -> String
forall a. Show a => a -> String
show Int
expectedRowCount
)
| Int
owned Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= Int
ownedDenominator =
String -> Either String [Row]
forall a b. a -> Either a b
Left
( String
"inventory owns "
String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Int -> String
forall a. Show a => a -> String
show Int
owned
String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
" rows, expected owned denominator "
String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Int -> String
forall a. Show a => a -> String
show Int
ownedDenominator
)
| [Text] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length ([Text] -> [Text]
forall a. Eq a => [a] -> [a]
nub [Text]
ids) Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= [Text] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
ids =
String -> Either String [Row]
forall a b. a -> Either a b
Left String
"inventory has duplicate row ids"
| (Row -> Bool) -> [Row] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> Bool
T.null (Text -> Bool) -> (Row -> Text) -> Row -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Row -> Text
rowGroup) [Row]
rows =
String -> Either String [Row]
forall a b. a -> Either a b
Left String
"inventory has a row with an empty group"
| Bool
otherwise = [Row] -> Either String [Row]
forall a b. b -> Either a b
Right [Row]
rows
where
ids :: [Text]
ids = (Row -> Text) -> [Row] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Row -> Text
rowId [Row]
rows
owned :: Int
owned =
[Row] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length
((Row -> Bool) -> [Row] -> [Row]
forall a. (a -> Bool) -> [a] -> [a]
filter ((RowState -> RowState -> Bool
forall a. Eq a => a -> a -> Bool
/= RowState
OutOfScope) (RowState -> Bool) -> (Row -> RowState) -> Row -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Row -> RowState
rowState) [Row]
rows)
data ShownState
= ShownExecuted
| ShownPartial
| ShownRecorded Verdict
| ShownPlanned RowState
deriving stock (Int -> ShownState -> ShowS
[ShownState] -> ShowS
ShownState -> String
(Int -> ShownState -> ShowS)
-> (ShownState -> String)
-> ([ShownState] -> ShowS)
-> Show ShownState
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ShownState -> ShowS
showsPrec :: Int -> ShownState -> ShowS
$cshow :: ShownState -> String
show :: ShownState -> String
$cshowList :: [ShownState] -> ShowS
showList :: [ShownState] -> ShowS
Show, ShownState -> ShownState -> Bool
(ShownState -> ShownState -> Bool)
-> (ShownState -> ShownState -> Bool) -> Eq ShownState
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ShownState -> ShownState -> Bool
== :: ShownState -> ShownState -> Bool
$c/= :: ShownState -> ShownState -> Bool
/= :: ShownState -> ShownState -> Bool
Eq)
effectiveState :: Text -> [Receipt] -> Row -> ShownState
effectiveState :: Text -> [Receipt] -> Row -> ShownState
effectiveState Text
base [Receipt]
receipts Row
row =
case [ Receipt
r
| Receipt
r <- [Receipt]
receipts
, Receipt -> Text
receiptRow Receipt
r Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Row -> Text
rowId Row
row
, Receipt -> Text
receiptBase Receipt
r Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
base
] of
(Receipt
r : [Receipt]
_) -> case Receipt -> Verdict
receiptVerdict Receipt
r of
Verdict
AgreesWithModel -> ShownState
ShownExecuted
Verdict
Partial -> ShownState
ShownPartial
Verdict
other -> Verdict -> ShownState
ShownRecorded Verdict
other
[] -> RowState -> ShownState
ShownPlanned (Row -> RowState
rowState Row
row)
renderInventory :: Text -> [Receipt] -> [Row] -> Text
renderInventory :: Text -> [Receipt] -> [Row] -> Text
renderInventory Text
base [Receipt]
receipts [Row]
rows =
[Text] -> Text
T.unlines
( Text
header
Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
: (Row -> Text) -> [Row] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map ((Row, ShownState) -> Text
renderRow ((Row, ShownState) -> Text)
-> (Row -> (Row, ShownState)) -> Row -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Row -> (Row, ShownState)
shown) [Row]
sorted
[Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text
"", Text
summary, Text
ownedLine]
)
where
sorted :: [Row]
sorted = [Row] -> [Row]
forall a. Ord a => [a] -> [a]
sort [Row]
rows
shown :: Row -> (Row, ShownState)
shown Row
r = (Row
r, Text -> [Receipt] -> Row -> ShownState
effectiveState Text
base [Receipt]
receipts Row
r)
header :: Text
header =
Text
"id\tgroup\texpected\tstate\trequirement"
states :: [ShownState]
states =
[ ShownState
ShownExecuted
, ShownState
ShownPartial
, Verdict -> ShownState
ShownRecorded Verdict
HeldQ002
, Verdict -> ShownState
ShownRecorded Verdict
DivergesFromLean
, Verdict -> ShownState
ShownRecorded Verdict
ResolvedByRuling
, Verdict -> ShownState
ShownRecorded Verdict
UnmetByRuling
, RowState -> ShownState
ShownPlanned RowState
BoundElsewhere
, RowState -> ShownState
ShownPlanned RowState
Uncovered
, RowState -> ShownState
ShownPlanned RowState
OutOfScope
]
summary :: Text
summary =
String -> Text
T.pack (Int -> String
forall a. Show a => a -> String
show ([Row] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Row]
rows))
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" rows: "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
", " ((ShownState -> Text) -> [ShownState] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map ShownState -> Text
countFor [ShownState]
states)
countFor :: ShownState -> Text
countFor ShownState
s =
String -> Text
T.pack
( Int -> String
forall a. Show a => a -> String
show
( [Row] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length
( (Row -> Bool) -> [Row] -> [Row]
forall a. (a -> Bool) -> [a] -> [a]
filter
((ShownState -> ShownState -> Bool
forall a. Eq a => a -> a -> Bool
== ShownState
s) (ShownState -> Bool) -> (Row -> ShownState) -> Row -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Receipt] -> Row -> ShownState
effectiveState Text
base [Receipt]
receipts)
[Row]
rows
)
)
)
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> ShownState -> Text
shownName ShownState
s
ownedLine :: Text
ownedLine =
Text
"owned denominator: "
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> String -> Text
T.pack (Int -> String
forall a. Show a => a -> String
show Int
ownedDenominator)
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ("
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate
Text
", "
[ Row -> Text
rowId Row
r
| Row
r <- [Row]
rows
, Row -> RowState
rowState Row
r RowState -> RowState -> Bool
forall a. Eq a => a -> a -> Bool
== RowState
OutOfScope
]
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" out of scope)"
renderRow :: (Row, ShownState) -> Text
renderRow :: (Row, ShownState) -> Text
renderRow (Row
r, ShownState
s) =
Text -> [Text] -> Text
T.intercalate
Text
"\t"
[ Row -> Text
rowId Row
r
, Row -> Text
rowGroup Row
r
, Row -> Text
rowExpected Row
r
, ShownState -> Text
shownName ShownState
s
, Row -> Text
rowRequirement Row
r
]
shownName :: ShownState -> Text
shownName :: ShownState -> Text
shownName ShownState
ShownExecuted = Text
"executed"
shownName ShownState
ShownPartial = Text
"partial"
shownName (ShownRecorded Verdict
HeldQ002) = Text
"held"
shownName (ShownRecorded Verdict
DivergesFromLean) = Text
"diverges"
shownName (ShownRecorded Verdict
ResolvedByRuling) = Text
"resolved-by-ruling"
shownName (ShownRecorded Verdict
UnmetByRuling) = Text
"unmet-by-ruling"
shownName (ShownRecorded Verdict
AgreesWithModel) = Text
"executed"
shownName (ShownRecorded Verdict
Partial) = Text
"partial"
shownName (ShownPlanned RowState
Uncovered) = Text
"uncovered"
shownName (ShownPlanned RowState
BoundElsewhere) = Text
"bound-elsewhere"
shownName (ShownPlanned RowState
OutOfScope) = Text
"out-of-scope"
instance Ord Row where
compare :: Row -> Row -> Ordering
compare Row
a Row
b = Text -> Text -> Ordering
forall a. Ord a => a -> a -> Ordering
compare (Row -> Text
rowId Row
a) (Row -> Text
rowId Row
b)