packages feed

hanalyze-0.2.0.0: src/Hanalyze/DataIO/Log.hs

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Hanalyze.DataIO.Log
-- Description : データローダ / 前処理が共有する構造化警告・情報メッセージ (LogEntry/LogReport)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Structured warning / informational messaging shared by data loaders
-- and preprocessing.
--
--   * 'LogEntry'        — a single message (severity / code / body / hint).
--   * @LogReport@       — a 'Monoid' wrapper around @[LogEntry]@.
--   * 'Loaded'          — the @(value, log)@ pair returned by every loader.
--   * 'printLogReport'  — stdout pretty printer.
--   * @logEntriesAsHtml@ — adapter for 'Hanalyze.Viz.ReportBuilder'.
--
-- 利用シナリオ:
--
-- @
-- (df, lg) <- loadCsvSafe path  -- :: IO (Either ParseError (Loaded DataFrame))
-- printLogReport lg              -- 警告を端末に出す
-- when (isStrict opts && hasErrors lg) $ exitFailure
-- @
module Hanalyze.DataIO.Log
  ( -- * 型
    Severity (..)
  , LogEntry (..)
  , LogReport
  , Loaded
    -- * Construction
  , mkInfo
  , mkWarn
  , mkErr
  , addEntry
  , logReport
  , noLog
    -- * Aggregation
  , entries
  , hasErrors
  , hasWarnings
  , severityCount
    -- * Output
  , printLogReport
  , prettyEntry
  ) where

import Data.Text (Text)
import qualified Data.Text    as T
import qualified Data.Text.IO as TIO

-- ---------------------------------------------------------------------------
-- 型
-- ---------------------------------------------------------------------------

-- | Message severity.
data Severity = Info | Warn | Err
  deriving (Eq, Ord, Show)

-- | A single log entry.
--
-- 'lgCode' is a stable identifier of the form @W001@ / @E002@ used for
-- grepping output and writing tests against the log.
data LogEntry = LogEntry
  { lgSev  :: !Severity
  , lgCode :: !Text
  , lgMsg  :: !Text
  , lgHint :: !(Maybe Text)
  } deriving (Eq, Show)

-- | A 'Monoid' list-wrapper of 'LogEntry'.
newtype LogReport = LogReport { entries :: [LogEntry] }
  deriving (Eq, Show)

instance Semigroup LogReport where
  LogReport a <> LogReport b = LogReport (a ++ b)

instance Monoid LogReport where
  mempty = LogReport []

-- | A value paired with its log. Loaders and cleaners return this shape.
type Loaded a = (a, LogReport)

-- ---------------------------------------------------------------------------
-- 構築
-- ---------------------------------------------------------------------------

-- | Build an 'Info' entry from @(code, message, optional hint)@.
mkInfo :: Text -> Text -> Maybe Text -> LogEntry
mkInfo c m h = LogEntry Info c m h

-- | Build a @Warn@ entry.
mkWarn :: Text -> Text -> Maybe Text -> LogEntry
mkWarn c m h = LogEntry Warn c m h

-- | Build an 'Err' entry.
mkErr :: Text -> Text -> Maybe Text -> LogEntry
mkErr c m h = LogEntry Err c m h

-- | Append an entry to the end of a report.
addEntry :: LogEntry -> LogReport -> LogReport
addEntry e (LogReport xs) = LogReport (xs ++ [e])

-- | Make a @LogReport@ that contains a single entry.
logReport :: LogEntry -> LogReport
logReport e = LogReport [e]

-- | The empty log (alias for 'mempty').
noLog :: LogReport
noLog = mempty

-- ---------------------------------------------------------------------------
-- 集約
-- ---------------------------------------------------------------------------

-- | True if the report contains any 'Err' entries.
--
-- >>> hasErrors noLog
-- False
-- >>> hasErrors (logReport (mkErr "E001" "boom" Nothing))
-- True
hasErrors :: LogReport -> Bool
hasErrors (LogReport xs) = any ((== Err) . lgSev) xs

-- | True if the report contains any @Warn@ entries.
hasWarnings :: LogReport -> Bool
hasWarnings (LogReport xs) = any ((== Warn) . lgSev) xs

-- | Number of entries with the given severity.
severityCount :: Severity -> LogReport -> Int
severityCount s (LogReport xs) = length (filter ((== s) . lgSev) xs)

-- ---------------------------------------------------------------------------
-- 出力
-- ---------------------------------------------------------------------------

-- | Pretty-print a single 'LogEntry' (severity tag + code + message,
-- and optionally the hint on a second line).
prettyEntry :: LogEntry -> Text
prettyEntry e =
  let prefix = case lgSev e of
        Info -> "[INFO]  "
        Warn -> "[WARN]  "
        Err  -> "[ERROR] "
      hint = case lgHint e of
        Nothing -> ""
        Just h  -> "\n        ヒント: " <> h
  in prefix <> lgCode e <> ": " <> lgMsg e <> hint

-- | Print the log to stdout. Empty logs print nothing.
printLogReport :: LogReport -> IO ()
printLogReport (LogReport []) = return ()
printLogReport (LogReport xs) = do
  let nW = length (filter ((== Warn) . lgSev) xs)
      nE = length (filter ((== Err)  . lgSev) xs)
      nI = length (filter ((== Info) . lgSev) xs)
      summary = T.concat
        [ "(" , T.pack (show (length xs)), " entries"
        , if nE > 0 then ", " <> T.pack (show nE) <> " error"   else ""
        , if nW > 0 then ", " <> T.pack (show nW) <> " warning" else ""
        , if nI > 0 then ", " <> T.pack (show nI) <> " info"    else ""
        , ")"
        ]
  TIO.putStrLn ("--- DataIO log " <> summary <> " ---")
  mapM_ (TIO.putStrLn . prettyEntry) xs
  TIO.putStrLn "----------------------"