module Ecluse.Core.Registry.PyPI.StreamingProjection (
PyPIProjection,
emptyProjection,
collectField,
keepsFile,
finishProjection,
) where
import Data.Aeson (Value)
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (parseEither)
import Data.Set qualified as Set
import Ecluse.Core.Package (InvalidEntry, InvalidEntryKind (InvalidVersionListing), PackageInfo, PackageName, mkInvalidEntry)
import Ecluse.Core.Package.Entry (EntryKey)
import Ecluse.Core.Registry.Metadata (MetadataError (MetadataBoundExceeded, MetadataUndecodable))
import Ecluse.Core.Registry.Metadata.Projection (projectionResult, validateReportedName)
import Ecluse.Core.Registry.PyPI.Document (SimpleDocument, simpleDocument)
import Ecluse.Core.Registry.PyPI.Project (FileCoordinate (fcVersionKey), FilenameMemo, filenameMemo, projectName, projectSimpleIndex, readCoordinate)
import Ecluse.Core.Registry.PyPI.Streaming (PyPIField (..), PyPIRead (..))
import Ecluse.Core.Registry.PyPI.Wire (IndexFile (ifEntryKey, ifFilename), checkApiVersion, decodeIndexFiles)
import Ecluse.Core.Registry.WireSupport (checkNameAgreement)
import Ecluse.Core.Security (LimitError (TooManyArtifacts), Limits (maxArtifactCount), checkVersionCountOf)
data PyPIProjection = PyPIProjection
{ PyPIProjection -> KeyMap Value
projectedEnvelope :: KeyMap.KeyMap Value
, PyPIProjection -> [(IndexFile, Maybe FileCoordinate, Value)]
projectedFiles :: [(IndexFile, Maybe FileCoordinate, Value)]
, PyPIProjection -> FilenameMemo
projectedMemo :: FilenameMemo
, PyPIProjection -> [InvalidEntry]
projectedFileDrops :: [InvalidEntry]
, PyPIProjection -> [InvalidEntry]
projectedVersionDrops :: [InvalidEntry]
, PyPIProjection -> Set Text
projectedVersions :: Set Text
, PyPIProjection -> Int
projectedArtifactCount :: Int
, PyPIProjection -> Maybe LimitError
projectedBound :: Maybe LimitError
, PyPIProjection -> Bool
projectedShape :: Bool
, PyPIProjection -> Bool
projectedFilesSeen :: Bool
, PyPIProjection -> Bool
projectedFilesActive :: Bool
, PyPIProjection -> Bool
projectedVersionsSeen :: Bool
, PyPIProjection -> Bool
projectedVersionsActive :: Bool
}
emptyProjection :: PackageName -> PyPIProjection
emptyProjection :: PackageName -> PyPIProjection
emptyProjection PackageName
name = KeyMap Value
-> [(IndexFile, Maybe FileCoordinate, Value)]
-> FilenameMemo
-> [InvalidEntry]
-> [InvalidEntry]
-> Set Text
-> Int
-> Maybe LimitError
-> Bool
-> Bool
-> Bool
-> Bool
-> Bool
-> PyPIProjection
PyPIProjection KeyMap Value
forall a. Monoid a => a
mempty [] (PackageName -> FilenameMemo
filenameMemo PackageName
name) [] [] Set Text
forall a. Monoid a => a
mempty Int
0 Maybe LimitError
forall a. Maybe a
Nothing Bool
True Bool
False Bool
False Bool
False Bool
False
keepsFile :: PyPIProjection -> Bool
keepsFile :: PyPIProjection -> Bool
keepsFile PyPIProjection
acc = PyPIProjection -> Bool
projectedFilesActive PyPIProjection
acc Bool -> Bool -> Bool
&& Maybe LimitError -> Bool
forall a. Maybe a -> Bool
isNothing (PyPIProjection -> Maybe LimitError
projectedBound PyPIProjection
acc)
collectField :: Limits -> PyPIRead -> PyPIProjection -> PyPIField -> Either LimitError PyPIProjection
collectField :: Limits
-> PyPIRead
-> PyPIProjection
-> PyPIField
-> Either LimitError PyPIProjection
collectField Limits
limits PyPIRead
mode PyPIProjection
acc =
PyPIProjection -> Either LimitError PyPIProjection
forall a b. b -> Either a b
Right (PyPIProjection -> Either LimitError PyPIProjection)
-> (PyPIField -> PyPIProjection)
-> PyPIField
-> Either LimitError PyPIProjection
forall b c a. (b -> c) -> (a -> b) -> a -> c
. \case
PyPIField
IgnoredField -> PyPIProjection
acc
EnvelopeField Text
key Value
value
| Key -> KeyMap Value -> Bool
forall a. Key -> KeyMap a -> Bool
KeyMap.member (Text -> Key
Key.fromText Text
key) (PyPIProjection -> KeyMap Value
projectedEnvelope PyPIProjection
acc) -> PyPIProjection
acc
| Bool
otherwise -> PyPIProjection
acc{projectedEnvelope = KeyMap.insert (Key.fromText key) value (projectedEnvelope acc)}
FilesShape Bool
valid -> PyPIProjection
acc{projectedFilesSeen = True, projectedFilesActive = not (projectedFilesSeen acc), projectedShape = projectedShape acc && (projectedFilesSeen acc || valid)}
VersionsShape Bool
valid -> PyPIProjection
acc{projectedVersionsSeen = True, projectedVersionsActive = not (projectedVersionsSeen acc), projectedShape = projectedShape acc && (projectedVersionsSeen acc || valid)}
InvalidVersionField Int
position Value
value
| PyPIProjection -> Bool
projectedVersionsActive PyPIProjection
acc Bool -> Bool -> Bool
&& Maybe LimitError -> Bool
forall a. Maybe a -> Bool
isNothing (PyPIProjection -> Maybe LimitError
projectedBound PyPIProjection
acc) -> PyPIProjection
acc{projectedVersionDrops = mkInvalidEntry InvalidVersionListing (show position) value "expected a version string" : projectedVersionDrops acc}
| Bool
otherwise -> PyPIProjection
acc
FileField Int
position Maybe Value
raw
| PyPIProjection -> Bool
projectedFilesActive PyPIProjection
acc -> Int -> Maybe Value -> PyPIProjection
collectFile Int
position Maybe Value
raw
| Bool
otherwise -> PyPIProjection
acc
where
collectFile :: Int -> Maybe Value -> PyPIProjection
collectFile Int
position Maybe Value
raw =
let count :: Int
count = Int
position Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
bounded :: PyPIProjection
bounded = case PyPIRead
mode of
PyPIRead
FullRead -> PyPIProjection
acc
SelectedRead{} -> Either LimitError () -> PyPIProjection -> PyPIProjection
forall {b}. Either LimitError b -> PyPIProjection -> PyPIProjection
withBound (Limits -> Int -> Either LimitError ()
checkVersionCountOf Limits
limits Int
count) PyPIProjection
acc
in case Maybe Value
raw of
Just Value
value | Maybe LimitError -> Bool
forall a. Maybe a -> Bool
isNothing (PyPIProjection -> Maybe LimitError
projectedBound PyPIProjection
bounded) Bool -> Bool -> Bool
|| PyPIRead
mode PyPIRead -> PyPIRead -> Bool
forall a. Eq a => a -> a -> Bool
== PyPIRead
FullRead -> Int -> Value -> PyPIProjection -> PyPIProjection
retainFile Int
position Value
value PyPIProjection
bounded
Maybe Value
_ -> PyPIProjection
bounded
retainFile :: Int -> Value -> PyPIProjection -> PyPIProjection
retainFile Int
position Value
value PyPIProjection
current = (PyPIProjection -> IndexFile -> PyPIProjection)
-> PyPIProjection -> [IndexFile] -> PyPIProjection
forall b a. (b -> a -> b) -> b -> [a] -> b
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' (Value -> PyPIProjection -> IndexFile -> PyPIProjection
retain Value
value) PyPIProjection
withDrops [IndexFile]
files
where
([IndexFile]
files, [InvalidEntry]
drops) = [(Int, Value)] -> ([IndexFile], [InvalidEntry])
decodeIndexFiles [(Int
position, Value
value)]
withDrops :: PyPIProjection
withDrops
| Maybe LimitError -> Bool
forall a. Maybe a -> Bool
isNothing (PyPIProjection -> Maybe LimitError
projectedBound PyPIProjection
current) = PyPIProjection
current{projectedFileDrops = reverse drops <> projectedFileDrops current}
| Bool
otherwise = PyPIProjection
current
retain :: Value -> PyPIProjection -> IndexFile -> PyPIProjection
retain Value
value PyPIProjection
current IndexFile
file =
let (Maybe FileCoordinate
coordinate, FilenameMemo
memo) = FilenameMemo -> Text -> (Maybe FileCoordinate, FilenameMemo)
readCoordinate (PyPIProjection -> FilenameMemo
projectedMemo PyPIProjection
current) (IndexFile -> Text
ifFilename IndexFile
file)
next :: PyPIProjection
next
| Maybe LimitError -> Bool
forall a. Maybe a -> Bool
isNothing (PyPIProjection -> Maybe LimitError
projectedBound PyPIProjection
current) = PyPIProjection
current{projectedFiles = (file, coordinate, value) : projectedFiles current, projectedMemo = memo}
| Bool
otherwise = PyPIProjection
current
in case (PyPIRead
mode, FileCoordinate -> Text
fcVersionKey (FileCoordinate -> Text) -> Maybe FileCoordinate -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe FileCoordinate
coordinate) of
(PyPIRead
FullRead, Just Text
version) ->
let versions :: Set Text
versions = Text -> Set Text -> Set Text
forall a. Ord a => a -> Set a -> Set a
Set.insert Text
version (PyPIProjection -> Set Text
projectedVersions PyPIProjection
current)
count :: Int
count = PyPIProjection -> Int
projectedArtifactCount PyPIProjection
current Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
bounded :: PyPIProjection
bounded = case Limits -> Int -> Either LimitError ()
checkVersionCountOf Limits
limits (Set Text -> Int
forall a. Set a -> Int
Set.size Set Text
versions) of
Left LimitError
fault -> PyPIProjection
next{projectedBound = Just fault}
Right () -> PyPIProjection
next{projectedVersions = versions, projectedArtifactCount = count}
in Either LimitError () -> PyPIProjection -> PyPIProjection
forall {b}. Either LimitError b -> PyPIProjection -> PyPIProjection
withBound (if Int
count Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Limits -> Int
maxArtifactCount Limits
limits then LimitError -> Either LimitError ()
forall a b. a -> Either a b
Left (Int -> Int -> LimitError
TooManyArtifacts Int
count (Limits -> Int
maxArtifactCount Limits
limits)) else () -> Either LimitError ()
forall a b. b -> Either a b
Right ()) PyPIProjection
bounded
(PyPIRead, Maybe Text)
_ -> PyPIProjection
next
withBound :: Either LimitError b -> PyPIProjection -> PyPIProjection
withBound Either LimitError b
result PyPIProjection
current = PyPIProjection
current{projectedBound = projectedBound current <|> either Just (const Nothing) result}
finishProjection :: PackageName -> PyPIProjection -> Either MetadataError (PackageInfo, SimpleDocument)
finishProjection :: PackageName
-> PyPIProjection
-> Either MetadataError (PackageInfo, SimpleDocument)
finishProjection PackageName
requested PyPIProjection
acc = do
(String -> MetadataError)
-> Either String () -> Either MetadataError ()
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (MetadataError -> String -> MetadataError
forall a b. a -> b -> a
const MetadataError
MetadataUndecodable) ((KeyMap Value -> Parser ()) -> KeyMap Value -> Either String ()
forall a b. (a -> Parser b) -> a -> Either String b
parseEither KeyMap Value -> Parser ()
checkApiVersion (PyPIProjection -> KeyMap Value
projectedEnvelope PyPIProjection
acc))
reported <- (Text -> Either ParseError PackageName)
-> Maybe Value -> Either MetadataError PackageName
validateReportedName Text -> Either ParseError PackageName
projectName (Key -> KeyMap Value -> Maybe Value
forall v. Key -> KeyMap v -> Maybe v
KeyMap.lookup Key
"name" (PyPIProjection -> KeyMap Value
projectedEnvelope PyPIProjection
acc))
_ <- projectionResult (checkNameAgreement requested reported ())
unless (projectedShape acc) (Left MetadataUndecodable)
traverse_ (Left . MetadataBoundExceeded) (projectedBound acc)
let invalid = [InvalidEntry] -> [InvalidEntry]
forall a. [a] -> [a]
reverse (PyPIProjection -> [InvalidEntry]
projectedFileDrops PyPIProjection
acc) [InvalidEntry] -> [InvalidEntry] -> [InvalidEntry]
forall a. Semigroup a => a -> a -> a
<> [InvalidEntry] -> [InvalidEntry]
forall a. [a] -> [a]
reverse (PyPIProjection -> [InvalidEntry]
projectedVersionDrops PyPIProjection
acc)
pure
( projectSimpleIndex reported invalid [(file, coordinate) | (file, coordinate, _) <- reverse (projectedFiles acc)]
, simpleDocument (projectedEnvelope acc) (servedFiles (projectedFiles acc))
)
servedFiles :: [(IndexFile, Maybe FileCoordinate, Value)] -> [(EntryKey, Value)]
servedFiles :: [(IndexFile, Maybe FileCoordinate, Value)] -> [(EntryKey, Value)]
servedFiles = ([(EntryKey, Value)]
-> (IndexFile, Maybe FileCoordinate, Value) -> [(EntryKey, Value)])
-> [(EntryKey, Value)]
-> [(IndexFile, Maybe FileCoordinate, Value)]
-> [(EntryKey, Value)]
forall b a. (b -> a -> b) -> b -> [a] -> b
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' (\[(EntryKey, Value)]
served (IndexFile
file, Maybe FileCoordinate
_, Value
value) -> let !key :: EntryKey
key = IndexFile -> EntryKey
ifEntryKey IndexFile
file in (EntryKey
key, Value
value) (EntryKey, Value) -> [(EntryKey, Value)] -> [(EntryKey, Value)]
forall a. a -> [a] -> [a]
: [(EntryKey, Value)]
served) []