packages feed

eventuo11y-json-0.3.0.3: src/Observe/Event/Render/JSON/DSL/Compile.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
#if ! MIN_VERSION_template_haskell(2,18,0)
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies #-}
#endif

-- |
-- Description : Compile the "Observe.Event.DSL" and generate "Observe.Event.Render.JSON" instances
-- Copyright   : Copyright 2022 Shea Levy.
-- License     : Apache-2.0
-- Maintainer  : shea@shealevy.com
module Observe.Event.Render.JSON.DSL.Compile (compile) where

import Data.Aeson
import GHC.Exts
import Language.Haskell.TH
import Observe.Event.DSL
import qualified Observe.Event.DSL.Compile as DSL
import Observe.Event.Render.JSON

#if ! MIN_VERSION_template_haskell(2,18,0)
type Quote m = m ~ Q
#endif

conPCompat :: Name -> [Pat] -> Pat
#if MIN_VERSION_template_haskell(2,18,0)
conPCompat n ps = ConP n [] ps
#else
conPCompat = ConP
#endif

-- | Compile a 'SelectorSpec' to type definitions, with a 'DefaultRenderSelectorJSON' instance.
--
-- Assumes leaf types (i.e., those in 'SimpleType' or a 'FieldConstructorSpec') are 'ToJSON', and
-- that 'Inject'ed selectors have a 'DefaultRenderSelectorJSON' instance.
compile :: (Quote m) => SelectorSpec -> m [Dec]
compile s@(SelectorSpec selectorNameBase selectors) = do
  -- Walks the selectors twice, will fix when SelectorSpec is extensible (e.g. recursion-schemes)
  baseDecs <- DSL.compile s
  let (renderSelectorClauses, decs) = foldr stepSelectors (mempty, baseDecs) selectors
      selectorInstance =
        InstanceD
          Nothing
          []
          (AppT (ConT ''DefaultRenderSelectorJSON) (ConT selectorName))
          [FunD 'defaultRenderSelectorJSON renderSelectorClauses]
  pure $ selectorInstance : decs
  where
    -- Deduplicate this with e11y-dsl when extending language
    selectorName = mkName $ upperCamel selectorNameBase <> "Selector"

    stepSelectors (SelectorConstructorSpec nm NoFields) (renderSelectorClauses, decs) = (c : renderSelectorClauses, decs)
      where
        c =
          Clause
            [conPCompat (mkName $ upperCamel nm) []]
            ( NormalB
                ( TupE
                    [ Just . LitE $ StringL (kebab nm),
                      Just $ VarE 'defaultRenderFieldJSON
                    ]
                )
            )
            []
    stepSelectors (SelectorConstructorSpec nm (Inject _)) (renderSelectorClauses, decs) = (c : renderSelectorClauses, decs)
      where
        keyNm = mkName "key"
        renderNm = mkName "render"
        selNm = mkName "sel"
        c =
          Clause
            [conPCompat (mkName $ upperCamel nm) [VarP selNm]]
            ( NormalB
                ( TupE
                    [ Just $ InfixE (Just . LitE $ StringL (kebab nm <> ":")) (VarE '(<>)) (Just $ VarE keyNm),
                      Just $ VarE renderNm
                    ]
                )
            )
            [ ValD (TupP [VarP keyNm, VarP renderNm]) (NormalB $ AppE (VarE 'defaultRenderSelectorJSON) (VarE selNm)) []
            ]
    -- TODO handle case where field is not ToJSON
    stepSelectors (SelectorConstructorSpec nm (SimpleType _)) (renderSelectorClauses, decs) = (c : renderSelectorClauses, decs)
      where
        xNm = mkName "x"
        c =
          Clause
            [conPCompat (mkName $ upperCamel nm) []]
            ( NormalB
                ( TupE
                    [ Just . LitE $ StringL (kebab nm),
                      Just
                        . LamE
                          [ VarP xNm
                          ]
                        $ TupE
                          [ Just . LitE $ StringL "val", -- Extend SimpleType with field name
                            Just $ AppE (VarE 'toJSON) (VarE xNm)
                          ]
                    ]
                )
            )
            []
    stepSelectors (SelectorConstructorSpec nm (Specified fieldSpec)) (renderSelectorClauses, decs) = (c : renderSelectorClauses, fieldDec : decs)
      where
        c =
          Clause
            [conPCompat (mkName $ upperCamel nm) []]
            ( NormalB
                ( TupE
                    [ Just . LitE $ StringL (kebab nm),
                      Just $ VarE 'defaultRenderFieldJSON
                    ]
                )
            )
            []
        fieldDec = compileFieldSpec fieldSpec

compileFieldSpec :: FieldSpec -> Dec
compileFieldSpec (FieldSpec fieldNameBase fields) =
  InstanceD
    Nothing
    []
    (AppT (ConT ''DefaultRenderFieldJSON) (ConT fieldName))
    [FunD 'defaultRenderFieldJSON (renderFieldClause <$> fields)]
  where
    -- Deduplicate this with e11y-dsl when extending language
    fieldName = mkName $ upperCamel fieldNameBase <> "Field"
    renderFieldClause (FieldConstructorSpec ctorNm ts) =
      Clause
        [conPCompat (mkName $ upperCamel ctorNm) (map VarP argNms)]
        ( NormalB
            ( TupE
                [ Just . LitE $ StringL (kebab ctorNm),
                  Just $ valE
                ]
            )
        )
        []
      where
        len = length ts
        argNms = fmap (\idx -> mkName $ "x" <> show idx) [1 .. len]
        valE = case argNms of
          [] -> ConE 'Null
          [argNm] -> AppE (VarE 'toJSON) (VarE argNm)
          _ ->
            AppE
              (ConE 'Object)
              ( AppE
                  (VarE 'fromList)
                  ( ListE . fst $
                      foldr
                        ( \argNm (jsonEs, idx) ->
                            ( TupE
                                [ Just . LitE $ StringL (show idx),
                                  Just $ AppE (VarE 'toJSON) (VarE argNm)
                                ]
                                : jsonEs,
                              idx - 1
                            )
                        )
                        ([], len)
                        argNms
                  )
              )