packages feed

eventuo11y-dsl-0.2.0.0: src/Observe/Event/DSL.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
-- Duplication due to compatibility for AnyQuote
{-# OPTIONS_GHC -Wno-duplicate-exports #-}

-- |
-- Description : DSL for generating 'Observe.Event.Event' fields and selectors
-- Copyright   : Copyright 2022 Shea Levy.
-- License     : Apache-2.0
-- Maintainer  : shea@shealevy.com
--
-- DSL for generating 'Observe.Event.Event' fields and selectors.
--
-- Typical entrypoint is 'SelectorSpec'.
--
-- See [Example.hs](https://github.com/shlevy/eventuo11y/tree/v0.6.0.0/Example.hs) for an idiomatic example.
--
-- See "Observe.Event.DSL.Compile" to compile this into the relevant types.
module Observe.Event.DSL
  ( -- * The core AST
    SelectorSpec (..),
    SelectorConstructorSpec (..),
    SelectorField (..),
    FieldSpec (..),
    FieldConstructorSpec (..),

    -- * Syntax
    RecordField (..),

    -- * Miscellaneous helpers

    -- ** Quote polymorphism
    AnyQuote (..),
    toQuote,
    AnyType,

    -- ** Names
    ExplodedName,
    upperCamel,
    lowerCamel,
    kebab,
    NonEmptyString ((:|:)),
    nonEmptyToString,
  )
where

#if MIN_VERSION_template_haskell(2,18,0)
import Control.Applicative
#endif
import Data.Char
import Data.List
import Data.List.NonEmpty hiding (fromList, toList)
import Data.String
import GHC.Exts
import Language.Haskell.TH
import Observe.Event.Syntax

-- | A specification for an 'Observe.Event.Event' selector type
data SelectorSpec
  = SelectorSpec
      !ExplodedName
      -- ^ The base name of the generated type. @Selector@ will be appended.
      ![SelectorConstructorSpec]
      -- ^ Constructors for the selector type.

-- | A specification for a single constructor for a selector
--
-- End users probably want to use 'RecordField' to create
-- 'SelectorConstructorSpec's.
data SelectorConstructorSpec
  = SelectorConstructorSpec
      !ExplodedName
      -- ^ The name of the constructor
      !SelectorField
      -- ^ The type of fields associated with this selector

-- | Ways to specify the field for a selector.
data SelectorField
  = -- | The field is itself specified with the DSL.
    --
    -- The field type will be generated alongside the selector type.
    --
    -- End users probably want to use the 'RecordField' 'ExplodedName'
    -- 'FieldSpec' 'SelectorConstructorSpec' instance for 'Specified'
    -- 'SelectorField's.
    Specified !FieldSpec
  | -- | The field type is simply a preexisting type, typically not eventuo11y-aware.
    --
    -- End users probably want to use the 'RecordField' 'ExplodedName'
    -- 'Name' 'SelectorConstructorSpec' or 'RecordField' 'ExplodedName'
    -- 'AnyType' 'SelectorConstructorSpec' instances for 'SimpleType'
    -- 'SelectorField's
    SimpleType !AnyType
  | -- | This selector is a natural injection from a different selector type.
    --
    -- This is typically used to call library code with its own selector types.
    Inject !Name
  | -- | Events selected by this selector have no fields.
    --
    -- This may be useful purely to add timing to some event, or
    -- to create an event that is parent and/or proximate to other
    -- events.
    NoFields

-- | A specification for an 'Observe.Event.Event' field type.
data FieldSpec
  = FieldSpec
      !ExplodedName
      -- ^ The base name of the field. @Field@ will be appended.
      ![FieldConstructorSpec]
      -- ^ Constructors of this field type.

-- | A specification for a single constructor for a field
--
-- End users probably want to use 'RecordField' to create
-- 'FieldConstructorSpec's.
data FieldConstructorSpec
  = FieldConstructorSpec
      !ExplodedName
      -- ^ The name of the constructor
      !(NonEmpty AnyType)
      -- ^ The types of the arguments to the constructor.

-- | e.g. @"foo" ≔ NoFields@
instance (a ~ ExplodedName) => RecordField a SelectorField SelectorConstructorSpec where
  (≔) = SelectorConstructorSpec

-- | e.g. @"foo" ≔ FieldSpec ...@
instance (a ~ ExplodedName) => RecordField a FieldSpec SelectorConstructorSpec where
  k ≔ v = k ≔ Specified v

-- | e.g. @"foo" ≔ [t|Maybe Int|]@
instance (a ~ ExplodedName, m ~ AnyQuote) => RecordField a (m Type) SelectorConstructorSpec where
  k ≔ v = k ≔ SimpleType v

-- | e.g. @"foo" ≔ ''Int@
instance (a ~ ExplodedName) => RecordField a Name SelectorConstructorSpec where
  k ≔ v = k ≔ (pure $ ConT v)

-- | e.g. @"foo" ≔ [t|Int] :| [ [t|Bool], [t|Char] ]@
instance (a ~ ExplodedName, m ~ AnyQuote) => RecordField a (NonEmpty (m Type)) FieldConstructorSpec where
  (≔) = FieldConstructorSpec

-- | e.g. @"foo" ≔ [t|Maybe Int]@
instance (a ~ ExplodedName, m ~ AnyQuote) => RecordField a (m Type) FieldConstructorSpec where
  k ≔ v = k ≔ (v :| [])

-- | e.g. @"foo" ≔ [''Int, ''Char]@
instance (a ~ ExplodedName) => RecordField a [Name] FieldConstructorSpec where
  k ≔ v = k ≔ (pure . ConT <$> fromList @(NonEmpty _) v)

-- | e.g. @"foo" ≔ ''Int@
instance (a ~ ExplodedName) => RecordField a Name FieldConstructorSpec where
  k ≔ v = k ≔ ((pure $ ConT v) :| [])

#if MIN_VERSION_template_haskell(2,18,0)
-- | A concrete type for TH quotes that retains full 'Quote' polymorphism
--
-- Prior to @template-haskell@ @2.18@, this is just an alias for 'Q'
newtype AnyQuote a = AnyQuote
  { -- | Extract this value in a particular 'Quote' monad
    --
    -- Prior to @template-haskell@ @2.18@, this projects into 'Q'.
    toQuote :: forall m. Quote m => m a
  } deriving (Functor)

instance Applicative AnyQuote where
  pure x = AnyQuote $ pure x
  (AnyQuote f) <*> (AnyQuote x) = AnyQuote $ f <*> x
  liftA2 f (AnyQuote x) (AnyQuote y) = AnyQuote $ liftA2 f x y
  (AnyQuote x) *> (AnyQuote y) = AnyQuote $ x *> y
  (AnyQuote x) <* (AnyQuote y) = AnyQuote $ x <* y

instance Monad AnyQuote where
  (AnyQuote x) >>= f = AnyQuote $ do
    x' <- x
    let AnyQuote res = f x'
    res

instance Quote AnyQuote where
  newName s = AnyQuote $ newName s

#else
-- | A type alias for TH quotes
type AnyQuote = Q

toQuote :: AnyQuote a -> Q a
toQuote = id
#endif

-- | A 'Type' in any 'Quote'
type AnyType = AnyQuote Type

-- | A name for some element, broken up into words.
--
-- Different elements will use this differently. For example, using
-- @[ "foo", "bar" ]@ in a 'SelectorSpec' would result in a type named
-- @FooBarSelector@, while using it in a 'FieldSpec' might cause a
-- renderer generator to give the field the key @foo-bar@.
newtype ExplodedName = ExplodedName (NonEmpty NonEmptyString)

-- | Must be non-empty.
instance IsList ExplodedName where
  type Item ExplodedName = NonEmptyString
  fromList = coerce . fromList @(NonEmpty _)
  toList = toList @(NonEmpty _) . coerce

-- | A singleton 'ExplodedName'.
instance IsString ExplodedName where
  fromString = fromList . (: []) . fromString

-- | Convert an 'ExplodedName' to UpperCamelCase.
upperCamel :: (IsList a, Item a ~ NonEmptyString) => a -> String
upperCamel = concat . fmap (\(hd :|: tl) -> toUpper hd : tl) . toList

-- | Convert an 'ExplodedName' to lowerCamelCase
lowerCamel :: ExplodedName -> String
lowerCamel (ExplodedName ((hd :|: tl) :| rest)) =
  (toLower hd : tl)
    <> upperCamel rest

-- | Convert an 'ExplodedName' to kebab-case
kebab :: ExplodedName -> String
kebab = intercalate "-" . fmap (\(hd :|: tl) -> toLower hd : tl) . toList

-- | Self-explanatory
newtype NonEmptyString = NonEmptyString (NonEmpty Char)

-- | Must be non-empty
instance IsList NonEmptyString where
  type Item NonEmptyString = Char
  fromList = coerce . fromList @(NonEmpty _)
  toList = toList @(NonEmpty _) . coerce

{-# COMPLETE (:|:) #-}

pattern (:|:) :: Char -> String -> NonEmptyString
pattern (:|:) hd tl <- NonEmptyString (hd :| tl)

-- | Must be non-empty
instance IsString NonEmptyString where
  fromString = fromList

-- | Self-explanatory
nonEmptyToString :: NonEmptyString -> String
nonEmptyToString = toList