packages feed

proto3-suite-0.5.0: tests/Test/Proto/Generate/Name/Gen.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
module Test.Proto.Generate.Name.Gen
  ( GenName (GenName, nameOcc, nameRes),
    protofile,
  )
where

import Hedgehog (MonadGen, Range)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range

import Control.Applicative
import Data.Char qualified as Char

-- -----------------------------------------------------------------------------

-- | 'GenName' is an association between generated names.
--
--   * 'nameOcc' is how the name occurs in the context of protocol buffers.
--
--   * 'nameRes' is a Haskell name that 'nameOcc' is expected to resolve as.
--
-- ==== __Example__
--
-- The *.proto filename "my_messages" should be resolved to the Haskell module
-- "MyMessages", so we would generate a 'GenName':
--
-- >>> GenName "my_messages" "MyMessages"
-- GenName {nameOcc = "my_messages", nameRes = "MyMessages"}
--
-- After an associated pair is generated, it can be used to test a renaming (call
-- it @f@) by checking:
--
-- prop> f (nameOcc nm) == nameRes nm
data GenName = GenName
  { nameOcc :: String
  , nameRes :: String
  }
  deriving (Eq, Show)

-- | Generate the name of a Protobuf file and the Haskell module it should be
-- resolved to.
protofile :: forall m. MonadGen m => Range Int -> m GenName
protofile len = do
  (psize, usize) <- Gen.sized \sz -> do
    x <- Gen.int (Range.linear 0 (fromIntegral sz))
    y <- Gen.int (Range.linear 0 (fromIntegral sz))
    pure (x, y)

  nm <- Gen.sized (ident . Range.linear 1 . fromIntegral)
  ps <- nameParts (Range.linear 0 psize)
  us <- underscores (Range.linear 0 usize)

  let nameOcc = nm ++ concatMap fst ps ++ us
  let nameRes = upperFirst nm ++ concatMap snd ps ++ us
  pure GenName {nameOcc, nameRes}
  where
    ident :: Range Int -> m String
    ident = liftA2 (:) Gen.alpha . alphaNum

    nameParts :: Range Int -> m [(String, String)]
    nameParts rng = Gen.list len do
      n <- Gen.sized (Gen.int . Range.linear 1 . fromIntegral)
      nm <- ident rng
      us <- underscores (Range.linear 1 (fromIntegral n))
      pure (us ++ nm, drop 1 us ++ upperFirst nm)

    upperFirst :: String -> String
    upperFirst (c : cs) = Char.toUpper c : cs
    upperFirst "" = ""

-- -----------------------------------------------------------------------------
--
-- Primitive Name Generation Combinators
--

-- | Generate a string made up of underscores.
underscores :: MonadGen m => Range Int -> m String
underscores rng = Gen.list rng (pure '_')

-- | Generate a name containing alphabetical and numeric characters
-- @'a' .. 'z'@, @'A' .. 'Z'@, and @'0' .. '1'@.
alphaNum :: MonadGen m => Range Int -> m String
alphaNum rng = Gen.list rng Gen.alphaNum