packages feed

scrod-0.2026.2.21: source/library/Scrod/Json/Object.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskellQuotes #-}

module Scrod.Json.Object where

import qualified Data.ByteString.Builder as Builder
import qualified Data.Text as Text
import qualified Scrod.Extra.Builder as Builder
import qualified Scrod.Extra.Monoid as Monoid
import qualified Scrod.Extra.Parsec as Parsec
import qualified Scrod.Extra.Semigroup as Semigroup
import qualified Scrod.Json.Pair as Pair
import qualified Scrod.Json.String as String
import qualified Scrod.Spec as Spec
import qualified Text.Parsec as Parsec

newtype Object a = MkObject
  { unwrap :: [Pair.Pair a]
  }
  deriving (Eq, Ord, Show)

decode :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m a -> Parsec.ParsecT s u m (Object a)
decode p =
  fmap MkObject
    . Parsec.between (Parsec.char '{' <* Parsec.many Parsec.blank) (Parsec.char '}')
    $ Parsec.sepBy (Pair.decode p <* Parsec.many Parsec.blank) (Parsec.char ',' <* Parsec.many Parsec.blank)

encode :: (a -> Builder.Builder) -> Object a -> Builder.Builder
encode b =
  Semigroup.around (Builder.charUtf8 '{') (Builder.charUtf8 '}')
    . Monoid.sepBy (Builder.charUtf8 ',')
    . fmap (Pair.encode b)
    . unwrap

spec :: (Applicative m, Monad n) => Spec.Spec m n -> n ()
spec s = do
  let object :: [(String, a)] -> Object a
      object = MkObject . fmap (\(n, v) -> Pair.MkPair (String.MkString $ Text.pack n) v)

  Spec.named s 'decode $ do
    let p :: (Parsec.Stream t m Char) => Parsec.ParsecT t u m Prelude.String
        p = Parsec.many1 Parsec.digit

    Spec.it s "succeeds with an empty object" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{}") . Just $ object []

    Spec.it s "succeeds with an empty object with blank space" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{ }") . Just $ object []

    Spec.it s "succeeds with a single pair" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{\"a\":1}") . Just $ object [("a", "1")]

    Spec.it s "succeeds with a single pair with blank space" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{ \"a\":1 }") . Just $ object [("a", "1")]

    Spec.it s "succeeds with multiple pairs" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{\"a\":1,\"b\":2}") . Just $ object [("a", "1"), ("b", "2")]

    Spec.it s "succeeds with multiple pairs with blank space" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{ \"a\":1 , \"b\":2 }") . Just $ object [("a", "1"), ("b", "2")]

    Spec.it s "fails with leading comma" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{,\"a\":1}") Nothing

    Spec.it s "fails with trailing comma" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{\"a\":1,}") Nothing

    Spec.it s "fails with extra comma" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{\"a\":1,,\"b\":2}") Nothing

    Spec.it s "fails with only comma" $ do
      Spec.assertEq s (Parsec.parseString (decode p) "{,}") Nothing

  Spec.named s 'encode $ do
    let b = Builder.integerDec

    Spec.it s "encodes empty object" $ do
      Spec.assertEq s (Builder.toString . encode b $ MkObject []) "{}"

    Spec.it s "encodes single pair" $ do
      Spec.assertEq s (Builder.toString . encode b $ object [("a", 1)]) "{\"a\":1}"

    Spec.it s "encodes multiple pairs" $ do
      Spec.assertEq s (Builder.toString . encode b $ object [("a", 1), ("b", 2)]) "{\"a\":1,\"b\":2}"