module Data.Aeson.Codec
  (
  -- * JSON codecs
     JSONCodec
  -- * JSON object codecs
  , ObjectParser, ObjectBuilder, ObjectCodec
  , entry, pair, obj
  ) where

import Control.Applicative
import Data.Aeson
import Data.Aeson.Types (Parser, Pair)
import Control.Monad.Reader
import Control.Monad.Writer
import Data.Default.Class
import qualified Data.Text as T
import Data.String

import Data.Codec

-- | JSON codec. This is just a `ToJSON`/`FromJSON` implementation wrapped up in newtypes.
-- Use `def` to get a `JSONCodec` for a `ToJSON`/`FromJSON` instance.
type JSONCodec a = ConcreteCodec Value Parser a

instance (ToJSON a, FromJSON a) => Default (JSONCodec a) where
  def = Codec (ReaderT parseJSON) (Const . toJSON)

type ObjectParser = ReaderT Object Parser
type ObjectBuilder = Const (Endo [ Pair ])

-- | A codec that parses values out of a given `Object`, and produces
-- key-value pairs into a new one.
type ObjectCodec a = Codec ObjectParser ObjectBuilder a

-- | Produce a key-value pair.
pair :: ToJSON a => T.Text -> a -> ObjectBuilder ()
pair key val = Const $ Endo ((key .= val):)

-- | Read\/write a given value from/to a given key in the current object, using a given sub-codec.
-- ObjectCodec's `IsString` instance is equal to `entry` `def`.
entry :: T.Text -> JSONCodec a -> ObjectCodec a
entry key cd = Codec
  { parse = ReaderT $ \o -> (o .: key) >>= parseVal cd
  , produce = pair key . produceVal cd
  }

-- | Turn an `ObjectCodec` into a `JSONCodec` with an expected name (see `withObject`).
obj :: String -> ObjectCodec a -> JSONCodec a
obj err (Codec r w) = concrete
  (withObject err $ runReaderT r)
  (\x -> object $ appEndo (getConst $ w x) [])

instance (ToJSON a, FromJSON a) => IsString (ObjectCodec a) where
  fromString s = entry (fromString s) def