slackblocks-3.1.0: src/Slackblocks/Json.hs
{-# LANGUAGE FlexibleInstances #-}
-- | Checked JSON ingress. Unknown modeled values are rejected; opaque metadata
-- remains ordinary Aeson 'Value' and is never traversed as Block Kit content.
module Slackblocks.Json (FromSlackJSON (..), decode, fromValue, decimalText) where
import Data.Aeson (Value (..))
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy qualified as B
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Scientific (FPFormat (Generic), Scientific, base10Exponent, coefficient, floatingOrInteger, formatScientific)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Slackblocks.Internal.JsonSyntax (checkJSONNumbers, encodableScientific, parseJSONBytes)
import Slackblocks.Validation
-- | Validate an Aeson value as a supported modeled type. It is not an arbitrary Slack event parser.
class FromSlackJSON a where
-- | Check nested fields and receiving context; return structured validation errors.
fromSlackJSON :: Value -> Result a
-- | Validate an already parsed value. Use decode to retain duplicate-key and UTF-8 checks on raw bytes.
fromValue :: (FromSlackJSON a) => Value -> Result a
fromValue = fromSlackJSON
-- | Exact numeric convenience for a number-input bound. No binary floating-point
-- conversion is performed; the wire representation remains a decimal string.
decimalText :: Scientific -> Text
decimalText value
| encodableScientific value = T.pack (formatScientific Generic Nothing value)
| otherwise = T.pack (show (coefficient value) <> "e" <> show (base10Exponent value))
-- | Decode UTF-8 JSON bytes, rejecting duplicate keys, trailing input, numeric
-- exponent overflow in Aeson's Scientific representation and invalid modeled values.
decode :: (FromSlackJSON a) => B.ByteString -> Result a
decode input = parseJSONBytes input >>= fromSlackJSON
instance FromSlackJSON Text where
fromSlackJSON (String value) = Right value
fromSlackJSON _ = failure TypeMismatch [] "type" "expected text"
instance FromSlackJSON Bool where
fromSlackJSON (Bool value) = Right value
fromSlackJSON _ = failure TypeMismatch [] "type" "expected a boolean"
instance FromSlackJSON Integer where
fromSlackJSON (Number value) = case floatingOrInteger value :: Either Double Integer of
Right integer -> Right integer
Left _ -> failure TypeMismatch [] "type" "expected an integer"
fromSlackJSON _ = failure TypeMismatch [] "type" "expected an integer"
instance FromSlackJSON Scientific where
fromSlackJSON value@(Number number) = checkJSONNumbers value >> Right number
fromSlackJSON _ = failure TypeMismatch [] "type" "expected a number"
instance (FromSlackJSON a) => FromSlackJSON [a] where
fromSlackJSON (Array values) = traverse (\(index, value) -> atIndex index (fromSlackJSON value)) (zip [0 ..] (V.toList values))
fromSlackJSON _ = failure TypeMismatch [] "type" "expected an array"
instance FromSlackJSON (Map Text Value) where
fromSlackJSON value@(Object fields) = do
checkJSONNumbers value
Right (Map.fromList [(Key.toText key, item) | (key, item) <- KM.toList fields])
fromSlackJSON _ = failure TypeMismatch [] "type" "expected an object"