-- Example of a dynamically generated FromJSON instance.
--
-- Can be useful when one needs to use a function with a
-- FromJSON constraint, but some detail about the
-- conversion from JSON is not known until runtime.
{-# LANGUAGE CPP #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson             -- from aeson
import qualified Data.Aeson.Key as Key
import Data.Aeson.Types (Parser)
import Data.Proxy             -- from tagged
import Data.Reflection        -- from reflection
#if !(MIN_VERSION_base(4,11,0))
import Data.Semigroup         -- from base
#endif
import Data.Text (Text, pack) -- from text

import Control.Applicative

-- These imports are only for constructing the example value
import Lens.Micro.Aeson (_Value)   -- form microlens-aeson
import Lens.Micro.Extras (preview) -- from microlens

data Foo = Foo
    {
        field1 :: Int
    ,   field2 :: Int
    } deriving (Show)

fooParser :: Text -> Object -> Parser Foo
fooParser prefix o = do
    Foo <$> o .: (Key.fromText prefix <> "field1") <*> o .: (Key.fromText prefix <> "field2")

-- A wrapper over Foo carrying a phantom type s
newtype J a s = J { runJ :: a }

-- If the phantom type s reifies the parsing function, we can
-- use reflect to recover the function and implement
-- our FromJSON instance for J.
instance Reifies s (Object -> Parser a) => FromJSON (J a s) where
    parseJSON (Object v) = J <$> reflect (Proxy :: Proxy s) v
    parseJSON _          = empty

-- Convince the compiler that the phantom type in the proxy
-- supplied by reify is the same as the phantom type in J.
--
-- Otherwise the FromJSON instance for J won't kick in.
asProxyJ :: Proxy s -> J a s -> J a s
asProxyJ _ = id

exampleJSON :: Value
exampleJSON = maybe Null id (preview _Value str)
  where
    str = "{ \"zzfield1\" : 5, \"zzfield2\" : 7 }"::Text

main :: IO ()
main = do
    putStrLn "Enter prefix for the fields: "
    -- "zz" must be entered for the parse to succeed
    prefix <- fmap pack getLine

    -- fromJSON uses the dynamically generated FromJSON instance
    let result = reify (fooParser prefix) $ \proxy ->
            -- We must eliminate the J newtype before returning
            -- because, thanks to parametricity,
            -- the phantom type cannot escape the callback.
            runJ . asProxyJ proxy <$> fromJSON exampleJSON

    putStrLn (show (result :: Result Foo))
