packages feed

jordan-0.2.0.0: test/Jordan/SpecDefs.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}

module Jordan.SpecDefs where

import Control.Applicative (Alternative ((<|>)))
import Data.Foldable
import Data.Functor.Contravariant
import Data.Functor.Contravariant.Divisible
import Data.Scientific (Scientific, scientific)
import Data.String (IsString (..))
import Data.Text (Text, pack)
import GHC.Generics (Generic)
import Jordan.FromJSON.Class
import Jordan.Generic.Options
import Jordan.ToJSON.Class
import Test.Hspec (Arg, Example, Spec, SpecWith, describe, fit, it, shouldBe)
import Text.RawString.QQ

newtype BasicStruct = BasicStruct {bar :: ()}
  deriving (Show, Eq, Ord)

goodBasic :: BasicStruct
goodBasic = BasicStruct ()

instance FromJSON BasicStruct where
  fromJSON = (BasicStruct <$> fromNull) <> fromObject
    where
      fromObject =
        parseObject $
          BasicStruct <$> parseField "bar"
      fromNull = parseNull

instance ToJSON BasicStruct where
  toJSON = serializeObject $ serializeFieldWith "bar" serializeNull

basicNull = pack [r| null |]

basicOneFieldNoSpace = pack [r|{"bar":null}|]

basicOneField = pack [r| { "bar": null } |]

basicEscaped = pack [r| { "\u0062\u0061\u0072": null }|]

basicEscapedNoSpace = pack [r|{"\u0062\u0061\u0072":null}|]

basicPartialEscape = pack [r| { "\u0062a\u0072": null } |]

basicExtraFields = pack [r| { "whatever": null, "bar": null, "baz": null } |]

basicArray = pack [r| [null, { "whatever": null, "bar": null }, null, null] |]

data TwoFields = TwoFields {one :: (), two :: ()}
  deriving (Show, Eq, Ord, Generic)

goodTwo :: TwoFields
goodTwo = TwoFields () ()

instance FromJSON TwoFields where
  fromJSON = parseObject $ TwoFields <$> parseField "one" <*> parseField "two"

twoSimple = pack [r| { "one"   :   null, "two": null } |]

twoScramble = pack [r| {"two":null,"one":null} |]

twoExtra =
  pack
    [r|
  {
    "ignore": [],
    "one": null,
    "four": [],
    "five": "test",
    "seven": {},
    "six": "why tho",
    "two": null,
    "three": {},
    "ignored": [1,2,3,4, { " foo " : null }]
  }
|]

twoScrambleExtra =
  pack
    [r|
  {
    "ignore": null,
    "bad": null,
    "two": null,
    "another": null,
    "yetAgain": null,
    "one": null
  }
|]

data GenericStruct = GenericStruct
  { firstLabel :: (),
    secondLabel :: [()]
  }
  deriving (Show, Read, Eq, Ord, Generic)

instance FromJSON GenericStruct

genericDefault = pack [r| { "firstLabel": null, "secondLabel": [null] } |]

data GenericSum
  = GenericBasic BasicStruct
  | GenericTwo TwoFields
  deriving (Show, Eq, Ord, Generic)

instance FromJSON GenericSum

sumBasic = pack [r| { "GenericBasic": null } |]

sumBasicObj = pack [r| { "GenericBasic": { "bar": null } } |]

sumTwo = pack [r| { "GenericTwo": { "one": null, "two": null, "bar": null } } |]

mismatchType = pack [r| { "GenericTwo": null } |]

data Coord = Coord {x :: !Double, y :: !Double}
  deriving (Show, Eq, Ord, Generic)

instance FromJSON Coord where
  fromJSON = fromObject <> fromArray
    where
      fromObject = parseObject $ Coord <$> parseField "x" <*> parseField "y"
      fromArray = parseTuple $ Coord <$> consumeItem <*> consumeItem

coordBasic = pack [r|{ "x": 10, "y": 10 } |]

coordReverse = pack [r| { "y": 11, "x": 12 } |]

coordExtra = pack [r| { "x": 1, "y": 2, "z": 3 } |]

coordNoSpaceDecimal = pack [r|{"x":0.0,"y":0.0}|]

coordTuple = pack [r| [1, 20] |]

data HomogenousCoord = HomogenousCoord {hx :: !Double, hy :: !Double, hz :: !Double}
  deriving (Show, Read, Eq, Ord, Generic)

instance FromJSON HomogenousCoord where
  fromJSON = fromObject <> fromObjectZero <> fromArray <> fromArrayZero
    where
      fromObject = parseObject $ mk <$> parseField "x" <*> parseField "y" <*> parseField "z"
      fromObjectZero = parseObject $ mk <$> parseField "x" <*> parseField "y" <*> pure 1.0
      fromArray = parseTuple $ mk <$> consumeItem <*> consumeItem <*> consumeItem
      fromArrayZero = parseTuple $ mk <$> consumeItem <*> consumeItem <*> pure 1.0
      mk = HomogenousCoord

data WeirdFeedback
  = GeneralFeedback Text
  | SpecificFeedback [(Text, Text)]
  deriving (Show, Eq)

instance FromJSON WeirdFeedback where
  fromJSON =
    parseObject (GeneralFeedback <$> parseField "general")
      <> (SpecificFeedback <$> parseDictionary fromJSON)

basicWritingSpec ::
  (forall val. (ToJSON val, Show val) => val -> Text) ->
  Spec
basicWritingSpec writeJSON = do
  describe "writing basic primitives" $ do
    it "writes nulls" $
      writeJSON () `shouldBe` "null"
    it "writes True to true" $
      writeJSON True `shouldBe` "true"
    it "writes False to false" $
      writeJSON False `shouldBe` "false"
    it "writes an empty array" $
      writeJSON ([] :: [()]) `shouldBe` "[]"
    it "writes an array with one item" $ do
      writeJSON [()] `shouldBe` "[null]"
    it "writes an array with two items" $ do
      writeJSON [(), ()] `shouldBe` "[null,null]"
    it "writes the number 1" $ do
      writeJSON (1 :: Int) `shouldBe` "1.0"
    it "writes the number 1.5" $ do
      writeJSON (1.5 :: Double) `shouldBe` "1.5"

  describe "writing text" $ do
    it "writes with no escapes" $ do
      writeJSON ("foo" :: Text) `shouldBe` [r|"foo"|]
    it "writes with an escaped quote" $ do
      writeJSON ("foo\"" :: Text) `shouldBe` [r|"foo\""|]
    it "writes with an escaped backslash" $ do
      writeJSON ("foo\\" :: Text) `shouldBe` [r|"foo\\"|]
  describe "writing a basic object" $ do
    it "writes properly" $ do
      writeJSON goodBasic `shouldBe` [r|{"bar": null}|]

basicParsingSpec ::
  (Example a) =>
  (forall val. (FromJSON val, Show val, Eq val) => Text -> val -> a) ->
  SpecWith (Arg a)
basicParsingSpec parseMatch = do
  let parseExample t v = it ("parses " <> show t <> " to " <> show v) $ t `parseMatch` v

  describe "parsing basic structure" $ do
    parseExample "true" True
    parseExample "false" False
    parseExample "null" ()
  describe "parsing numbers" $ do
    let m (t, e, i) = parseExample t $ scientific e i
    traverse_
      m
      [ ("0", 0, 0),
        ("1", 1, 0),
        ("20", 20, 0),
        ("190865", 190865, 0),
        ("-0", 0, 0),
        ("-1", -1, 0),
        ("-10.5", -105, -1),
        ("-1e100", -1, 100)
      ]
  describe "basic array parsing" $ do
    "[]" `parseExample` ([] :: [()])
    "[null ]" `parseExample` [()]
    "[null\n,null]" `parseExample` [(), ()]
    basicNull `parseExample` goodBasic
    basicArray `parseExample` replicate 4 goodBasic
  describe "basic object parsing" $ do
    basicOneFieldNoSpace `parseExample` goodBasic
    basicOneField `parseExample` goodBasic
    basicExtraFields `parseExample` goodBasic
    basicEscaped `parseExample` goodBasic
    basicEscapedNoSpace `parseExample` goodBasic
    basicPartialEscape `parseExample` goodBasic
  describe "parsing coordinates" $ do
    coordBasic `parseExample` Coord 10 10
    coordReverse `parseExample` Coord 12 11
    coordExtra `parseExample` Coord 1 2
    coordTuple `parseExample` Coord 1 20
    coordNoSpaceDecimal `parseExample` Coord 0 0
  describe "parsing homogenous coords" $ do
    "[1,2]" `parseExample` HomogenousCoord 1 2 1
    "[1,2,2]" `parseExample` HomogenousCoord 1 2 2
    "{ \"x\": 1, \"y\": 2, \"z\": 10 }" `parseExample` HomogenousCoord 1 2 10
  describe "string parsing" $ do
    "\"foo\\\\\"" `parseExample` ("foo\\" :: Text)
    "\"foo\\\"\"" `parseExample` ("foo\"" :: Text)
    "\"foo\\u2795\"" `parseExample` ("foo➕" :: Text)
    [r|"foo"|] `parseExample` ("foo" :: Text)
    [r|"foo\\\\"|] `parseExample` ([r|foo\\|] :: Text)
  describe "parsing a two-field object" $ do
    twoSimple `parseExample` goodTwo
    twoScramble `parseExample` goodTwo
    twoExtra `parseExample` goodTwo
    twoScrambleExtra `parseExample` goodTwo
  describe "parsing a generically-derived object" $ do
    genericDefault `parseExample` GenericStruct () [()]
  describe "parsing a generally-derived sum object" $ do
    sumBasic `parseExample` GenericBasic (BasicStruct ())
    sumBasicObj `parseExample` GenericBasic (BasicStruct ())
    sumTwo `parseExample` GenericTwo (TwoFields () ())
  describe "object parsing with possible weirdness" $ do
    pack [r| {"general": "it sucked", "foo": "bar"} |] `parseExample` GeneralFeedback "it sucked"
    pack [r| {"foo": "bar"} |] `parseExample` SpecificFeedback [("foo", "bar")]