packages feed

eo-phi-normalizer-0.3.0: src/Language/EO/Phi/Metrics/Data.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Language.EO.Phi.Metrics.Data where

import Data.Aeson (ToJSON (..), Value (..), withObject, withScientific, withText, (.:), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Types (FromJSON (..), Parser, parseFail)
import Data.Generics.Labels ()
import Data.List (groupBy, intercalate)
import Data.Scientific (toRealFloat)
import GHC.Generics (Generic)
import Language.EO.Phi.Rules.Common ()
import Language.EO.Phi.TH (deriveJSON)

data Metrics a = Metrics
  { formations :: a
  , dataless :: a
  , applications :: a
  , dispatches :: a
  }
  deriving stock (Show, Generic, Eq, Functor, Foldable, Traversable)

$(deriveJSON ''Metrics)

toListMetrics :: Metrics a -> [a]
toListMetrics = foldMap (: [])

instance Applicative Metrics where
  pure :: a -> Metrics a
  pure a =
    Metrics
      { formations = a
      , dataless = a
      , applications = a
      , dispatches = a
      }

  (<*>) :: Metrics (a -> b) -> Metrics a -> Metrics b
  x <*> y =
    Metrics
      { formations = x.formations y.formations
      , dataless = x.dataless y.dataless
      , applications = x.applications y.applications
      , dispatches = x.dispatches y.dispatches
      }

instance (Num a) => Num (Metrics a) where
  (+) :: Metrics a -> Metrics a -> Metrics a
  (+) x y = (+) <$> x <*> y
  (-) :: Metrics a -> Metrics a -> Metrics a
  (-) x y = (-) <$> x <*> y
  (*) :: Metrics a -> Metrics a -> Metrics a
  (*) x y = (*) <$> x <*> y
  negate :: Metrics a -> Metrics a
  negate = (negate <$>)
  abs :: Metrics a -> Metrics a
  abs = (abs <$>)
  signum :: Metrics a -> Metrics a
  signum = (signum <$>)
  fromInteger :: Integer -> Metrics a
  fromInteger x = pure $ fromInteger x

data SafeNumber a = SafeNumber'NaN | SafeNumber'Number a
  deriving stock (Functor)

-- >>> import Data.Aeson (decode')
--
-- >>> decode' @(SafeNumber Double) "\"NaN\""
-- Just NaN
--
-- >>> decode' @(SafeNumber Double) "3"
-- Just 3.0

instance (FromJSON a, RealFloat a) => FromJSON (SafeNumber a) where
  parseJSON :: Value -> Parser (SafeNumber a)
  parseJSON (String s) =
    withText
      "NaN"
      ( \case
          "NaN" -> pure SafeNumber'NaN
          _ -> parseFail "String is not a NaN"
      )
      (String s)
  parseJSON (Number n) =
    withScientific
      "Number"
      (pure . pure . toRealFloat)
      (Number n)
  parseJSON _ = parseFail "Value is not a NaN or a Number"

instance (ToJSON a) => ToJSON (SafeNumber a) where
  toJSON :: SafeNumber a -> Value
  toJSON (SafeNumber'Number a) = toJSON a
  toJSON SafeNumber'NaN = toJSON ("NaN" :: String)

instance (Show a) => Show (SafeNumber a) where
  show :: SafeNumber a -> String
  show (SafeNumber'Number a) = show a
  show SafeNumber'NaN = "NaN"

instance (Eq a) => Eq (SafeNumber a) where
  (==) :: SafeNumber a -> SafeNumber a -> Bool
  (==) (SafeNumber'Number x) (SafeNumber'Number y) = x == y
  (==) _ _ = False

instance (Ord a) => Ord (SafeNumber a) where
  (<=) :: SafeNumber a -> SafeNumber a -> Bool
  (SafeNumber'Number x) <= (SafeNumber'Number y) = x <= y
  _ <= SafeNumber'Number _ = True
  _ <= _ = False

instance Applicative SafeNumber where
  pure :: a -> SafeNumber a
  pure = SafeNumber'Number
  (<*>) :: SafeNumber (a -> b) -> SafeNumber a -> SafeNumber b
  (<*>) (SafeNumber'Number x') (SafeNumber'Number y') = SafeNumber'Number (x' y')
  (<*>) _ _ = SafeNumber'NaN

instance (Num a) => Num (SafeNumber a) where
  (+) :: SafeNumber a -> SafeNumber a -> SafeNumber a
  (+) x y = (+) <$> x <*> y
  (*) :: SafeNumber a -> SafeNumber a -> SafeNumber a
  (*) x y = (*) <$> x <*> y
  abs :: SafeNumber a -> SafeNumber a
  abs = (abs <$>)
  signum :: SafeNumber a -> SafeNumber a
  signum = (signum <$>)
  fromInteger :: Integer -> SafeNumber a
  fromInteger x = pure $ fromInteger x
  negate :: SafeNumber a -> SafeNumber a
  negate = (negate <$>)

instance (Fractional a, Eq a) => Fractional (SafeNumber a) where
  fromRational :: Rational -> SafeNumber a
  fromRational = SafeNumber'Number . fromRational
  (/) :: SafeNumber a -> SafeNumber a -> SafeNumber a
  (/) (SafeNumber'Number x) (SafeNumber'Number y) | y /= 0 = SafeNumber'Number (x / y)
  (/) _ _ = SafeNumber'NaN

type MetricsSafe a = Metrics (SafeNumber a)

instance (Fractional a, Eq a) => Fractional (MetricsSafe a) where
  fromRational :: Rational -> MetricsSafe a
  fromRational _ = 0
  (/) :: MetricsSafe a -> MetricsSafe a -> MetricsSafe a
  (/) x y = (/) <$> x <*> y

instance (Num a) => Semigroup (Metrics a) where
  (<>) :: Metrics a -> Metrics a -> Metrics a
  (<>) = (+)

instance (Num a) => Monoid (Metrics a) where
  mempty :: Metrics a
  mempty = 0

type MetricsCount = Metrics Int

data BindingMetrics = BindingMetrics
  { name :: String
  , metrics :: MetricsCount
  }
  deriving stock (Show, Generic, Eq)

$(deriveJSON ''BindingMetrics)

type Path = [String]

data BindingsByPathMetrics = BindingsByPathMetrics
  { path :: Path
  , bindingsMetrics :: [BindingMetrics]
  }
  deriving (Show, Generic, Eq)

-- >>> splitStringOn '.' "abra.cada.bra"
-- ["abra","cada","bra"]
--
-- >>> splitStringOn '.' ""
-- []
splitStringOn :: Char -> String -> Path
splitStringOn sep = filter (/= [sep]) . groupBy (\a b -> a /= sep && b /= sep)

splitPath :: String -> Path
splitPath = splitStringOn '.'

instance FromJSON BindingsByPathMetrics where
  parseJSON :: Value -> Parser BindingsByPathMetrics
  parseJSON = withObject "BindingsByPathMetrics" $ \obj -> do
    path <- splitPath <$> (obj .: "path")
    bindingsMetrics <- obj .: "bindings-metrics"
    pure BindingsByPathMetrics{..}

instance ToJSON BindingsByPathMetrics where
  toJSON :: BindingsByPathMetrics -> Value
  toJSON BindingsByPathMetrics{..} =
    Aeson.object
      [ "path" .= intercalate "." path
      , "bindings-metrics" .= bindingsMetrics
      ]

data ObjectMetrics = ObjectMetrics
  { bindingsByPathMetrics :: Maybe BindingsByPathMetrics
  , thisObjectMetrics :: MetricsCount
  }
  deriving (Show, Generic, Eq)

$(deriveJSON ''ObjectMetrics)

data ProgramMetrics = ProgramMetrics
  { bindingsByPathMetrics :: Maybe BindingsByPathMetrics
  , programMetrics :: MetricsCount
  }
  deriving (Show, Generic, Eq)

$(deriveJSON ''ProgramMetrics)