{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE ExistentialQuantification #-}
module Data.ComposableAssociation
    ( -- * Description
      -- $header

      -- * Core Types
      Association (..)
    , (:<>) (..)
    , WithAssociation

      -- * Helper Functions
    , withAssociation
    , asValue
    , reKey

      -- * Lens
    , _value
    , _assoc
    , _base

      -- * Generic Invalid Encoding Exception
    , ObjectEncodingException (..)
    ) where

import GHC.Generics
import Data.Proxy
import Control.Exception
import Data.Typeable

-- | A type representing a key-value association where the "key" itself exists only at the type level.
--
-- >>> let x = Association Proxy [1, 2, 3] :: Asssociation "type-level-key" [Int]
--
-- This type exists primarily as a way to "tag" data with a key for the purpose of serializing haskell data into
-- formats that have a key-value representation (ex: a JSON object).
--
-- The example above represents a serializable key-value pair with a key of @"type-level-key"@ and a value of @[1, 2, 3]@.
--
-- Storing the key as type-level information allows for unambiguous deserialization.
data Association key value = Association (Proxy key) value
                      deriving (Show, Eq, Generic, Functor, Foldable, Traversable)

-- | A type representing the composition of a base type (which can be serialized into a key-value structure) along with a key-value type.
--
-- This type exists as a way to compose a haskell value that has a key-value representation (ex: a haskell
-- record where its fields are keys to their values) with additional key-value associations into a single key-value
-- object.
--
-- This is intended for use with @Association@ to add additional key-values to a type for the purposes of
-- serialization/deserialization.
--
-- For example:
--
-- >>> data User = User { name :: String, age :: Int }
-- >>> let alice = User "Alice" 26
-- >>> let bob = User "Bob" 25
-- >>> let charlie = User "Charlie" 27
-- >>> let bobsFriends = [alice, charlie]
-- >>> bobAndFriends :: User :<> Association "friends" [User]
-- >>> let bobAndFriends = bob :<> Association Proxy bobsFriends
--
-- While @(bob, bobsFriends)@ contains the same values as @bobAndFriends@, it lacks information about how to combine
-- @bob@ and @bobsFriends@ together into a single serialized key-value object (as well as how to deserialize
-- that back into haskell values).
data base :<> assoc = base :<> assoc
                       deriving (Show, Eq, Generic, Functor, Foldable, Traversable)

-- | Type alias for the (:<>) type operator.
--
-- Useful if you don't like the TypeOperators extension.
type WithAssociation base assoc = base :<> assoc

-- | Function alias for the @(:<>)@ type constructor.
withAssociation :: a -> b -> WithAssociation a b
withAssociation = (:<>)

-- | @_value :: Lens value value' (Association key value) (Association key value')@
_value :: Functor f => (value -> f value') -> Association key value -> f (Association key value')
_value inj (Association key value) = Association key <$> inj value

-- | @_assoc :: Lens assoc assoc' (base :<> assoc) (base :<> assoc')@
_assoc :: Functor f => (assoc -> f assoc') -> base :<> assoc -> f (base :<> assoc')
_assoc inj (base :<> keyValue) = (:<>) base <$> inj keyValue

-- | @_base :: Lens base base' (base :<> assoc) (base' :<> assoc)@
_base :: Functor f => (base -> f base') -> base :<> assoc -> f (base' :<> assoc)
_base inj (base :<> assoc) =  flip (:<>) assoc <$> inj base

-- | Convenience function for creating associations.
--
-- This is especially useful when type-inference elsewhere in your program will determine the type of the Association.
--
-- >>> let x = asValue True :: Association "whatever-key" Bool
asValue :: obj -> Association key obj
asValue = Association Proxy

-- | Convenience function for changing the type of the @Association@'s key.
--
-- >>> let x = Association Proxy 10 :: Association "key-x" Int
-- >>> let y = reKey x :: Association "key-y" Int
reKey :: Association key obj -> Association key' obj
reKey (Association _ obj) = Association Proxy obj

-- | Generic encoding exception for when a @:<>@ "base" cannot be encoded as something object-like.
--
-- Each serialization should have a more specific version of this exception to convey information about the failure.
data ObjectEncodingException = forall e. Exception e => ObjectEncodingException e deriving Typeable

instance Show ObjectEncodingException where
  show (ObjectEncodingException e) = show e

instance Exception ObjectEncodingException

-- $header
-- This library exports core types, helper functions, and Lens's.
--
-- Unless you're implementing a serialization library (orphan instances for these types to implement
-- serialization/deserialization for some format) you probably don't want to import this package directly. Additional
-- packages in this namespace re-export this module along with their orphan instances for serialization.