packages feed

wireform-derive-0.2.0.0: test/Test/Derive/NameStyle.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Derive.NameStyle (tests) where

import Data.Char qualified as Char
import Data.Text qualified as T
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Syd
import Test.Syd.Hedgehog ()
import Wireform.Derive.NameStyle


tests :: Spec
tests =
  describe "NameStyle" $
    sequence_
      [ describe "applyStyle / golden" $
          sequence_
            [ it
                "snake personName"
                (applyStyle SnakeCase "personName" `shouldBe` "person_name")
            , it
                "snake HTTPRequest"
                (applyStyle SnakeCase "HTTPRequest" `shouldBe` "http_request")
            , it
                "snake httpRequest"
                (applyStyle SnakeCase "httpRequest" `shouldBe` "http_request")
            , it
                "snake-of-snake personName"
                (applyStyle SnakeCase "person_name" `shouldBe` "person_name")
            , it
                "kebab personName"
                (applyStyle KebabCase "personName" `shouldBe` "person-name")
            , it
                "kebab HTTPRequest"
                (applyStyle KebabCase "HTTPRequest" `shouldBe` "http-request")
            , it
                "camel of snake"
                (applyStyle CamelCase "person_name" `shouldBe` "personName")
            , it
                "camel idempotent"
                (applyStyle CamelCase "personName" `shouldBe` "personName")
            , it
                "pascal of snake"
                (applyStyle PascalCase "person_name" `shouldBe` "PersonName")
            , it
                "pascal idempotent"
                (applyStyle PascalCase "PersonName" `shouldBe` "PersonName")
            , it
                "upper snake"
                (applyStyle UpperSnake "personName" `shouldBe` "PERSON_NAME")
            , it
                "upper kebab"
                (applyStyle UpperKebab "personName" `shouldBe` "PERSON-NAME")
            , it
                "strip prefix present"
                (applyStyle (StripPrefix "person") "personName" `shouldBe` "Name")
            , it
                "strip prefix absent"
                (applyStyle (StripPrefix "other") "personName" `shouldBe` "personName")
            , it
                "strip prefix CI"
                (applyStyle (StripPrefixCI "PERSON") "personName" `shouldBe` "Name")
            , it
                "strip suffix present"
                (applyStyle (StripSuffix "Name") "personName" `shouldBe` "person")
            , it
                "compose strip + snake"
                ( applyStyle
                    (StripPrefix "person" `andThen` SnakeCase)
                    "personHttpRequest"
                    `shouldBe` "http_request"
                )
            , it
                "replace"
                (applyStyle (Replace "Name" "Label") "personName" `shouldBe` "personLabel")
            , it
                "drop / take"
                (applyStyle (DropChars 6 `andThen` TakeChars 2) "personName" `shouldBe` "Na")
            , it
                "Idiomatic falls through to NoStyle without resolution"
                (applyStyle Idiomatic "personName" `shouldBe` "personName")
            ]
      , describe "Idiomatic resolution" $
          sequence_
            [ it
                "JSON resolves to CamelCase"
                ( applyStyle (resolveIdiomatic "json" Idiomatic) "person_name"
                    `shouldBe` "personName"
                )
            , it
                "EDN resolves to KebabCase"
                ( applyStyle (resolveIdiomatic "edn" Idiomatic) "personName"
                    `shouldBe` "person-name"
                )
            , it
                "Proto resolves to CamelCase"
                ( applyStyle (resolveIdiomatic "proto" Idiomatic) "person_name"
                    `shouldBe` "personName"
                )
            , it
                "TOML resolves to SnakeCase"
                ( applyStyle (resolveIdiomatic "toml" Idiomatic) "personName"
                    `shouldBe` "person_name"
                )
            , it
                "YAML resolves to KebabCase"
                ( applyStyle (resolveIdiomatic "yaml" Idiomatic) "personName"
                    `shouldBe` "person-name"
                )
            , it
                "XML resolves to PascalCase"
                ( applyStyle (resolveIdiomatic "xml" Idiomatic) "person_name"
                    `shouldBe` "PersonName"
                )
            , it
                "CBOR resolves to NoStyle (verbatim)"
                ( applyStyle (resolveIdiomatic "cbor" Idiomatic) "personName"
                    `shouldBe` "personName"
                )
            , it
                "compose preserves outer style"
                ( applyStyle
                    ( resolveIdiomatic
                        "edn"
                        (StripPrefix "person" `andThen` Idiomatic)
                    )
                    "personHttpRequest"
                    `shouldBe` "http-request"
                )
            ]
      , describe "properties" $
          sequence_
            [ it "NoStyle is identity" $ property $ do
                t <- forAll genIdent
                applyStyle NoStyle t === t
            , it "Compose NoStyle a == a" $ property $ do
                t <- forAll genIdent
                applyStyle (Compose NoStyle SnakeCase) t === applyStyle SnakeCase t
            , it "snake then camel preserves alpha" $ property $ do
                t <- forAll genCamelIdent
                let snaked = applyStyle SnakeCase t
                    roundTripped = applyStyle CamelCase snaked
                T.toLower roundTripped === T.toLower t
            , it "snake produces lowercase output" $ property $ do
                t <- forAll genIdent
                let s = applyStyle SnakeCase t
                assert (T.all (\c -> not (Char.isUpper c)) s)
            , it "kebab produces lowercase output" $ property $ do
                t <- forAll genIdent
                let s = applyStyle KebabCase t
                assert (T.all (\c -> not (Char.isUpper c)) s)
            , it "applyStyle is total (never bottom)" $ property $ do
                t <- forAll genIdent
                let len = T.length (applyStyle SnakeCase t)
                assert (len >= 0)
            ]
      ]


-- ---------------------------------------------------------------------------
-- Generators
-- ---------------------------------------------------------------------------

-- Identifier-like text: letters, digits, underscores; non-empty.
genIdent :: MonadGen m => m T.Text
genIdent =
  T.pack
    <$> Gen.list
      (Range.linear 1 20)
      ( Gen.frequency
          [ (5, Gen.alpha)
          , (1, Gen.element ['_'])
          , (1, Gen.digit)
          ]
      )


-- camelCase identifier: starts lower, no underscores, alphanumeric only.
genCamelIdent :: MonadGen m => m T.Text
genCamelIdent = do
  first <- Gen.lower
  rest <- Gen.list (Range.linear 0 12) Gen.alphaNum
  pure (T.pack (first : rest))