packages feed

hq-json-0.1.1: src/HQ/Transformation/AST.hs

{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HQ.Transformation.AST
  ( inferTransformationType,
    TransformationType (..),
    TransformationTypeError (..),
  )
where

import Data.Aeson.Types (Value (..))
import HQ.Transformation
import HQ.Transformation.TransformationType
import Relude hiding (Compose, Const, many, not, or, some, subtract, toStrict)

-- | Mismatched 'Compose' steps, keeping the offender for reporting.
data TransformationTypeError
  = -- | Right output vs left input mismatch; offender kept for reporting.
    InvalidCompose Transformation ValueType ValueType
  | -- | An 'Or' branch does not produce a boolean; offender kept.
    InvalidOr Transformation ValueType
  | -- | An 'And' branch does not produce a boolean; offender kept.
    InvalidAnd Transformation ValueType
  | -- | A 'Xor' branch does not produce a boolean; offender kept.
    InvalidXor Transformation ValueType
  | -- | A @filter@ predicate does not produce a boolean; offender kept.
    InvalidFilter Transformation ValueType
  | -- | Division by zero; offender kept. Detected statically so it
    -- cannot explode mid-stream after partial output.
    InvalidDivideByZero Transformation
  deriving (Eq, Show)

-- | Infer a transformation's input/output types; 'Compose' steps must
-- line up (constants accept anything). Only the root type is ever
-- consumed, so no annotated tree is materialized.
inferTransformationType ::
  Transformation -> Either TransformationTypeError TransformationType
inferTransformationType = go
  where
    go (Add _) = pure $ TransformationType ValueNumber ValueNumber
    go (Multiply _) = pure $ TransformationType ValueNumber ValueNumber
    go (Subtract _) = pure $ TransformationType ValueNumber ValueNumber
    go (Divide n)
      | n == 0 = Left (InvalidDivideByZero (Divide n))
      | otherwise = pure $ TransformationType ValueNumber ValueNumber
    go (Lt _) = pure $ TransformationType ValueNumber ValueBool
    go (Lte _) = pure $ TransformationType ValueNumber ValueBool
    go (ConcatString _) = pure $ TransformationType ValueString ValueString
    go (ConcatArray _) = pure $ TransformationType ValueArray ValueArray
    go Trim = pure $ TransformationType ValueString ValueString
    go (Replace _ _) = pure $ TransformationType ValueString ValueString
    go (StripPrefix _) = pure $ TransformationType ValueString ValueString
    go (StripSuffix _) = pure $ TransformationType ValueString ValueString
    go (IsPrefixOf _) = pure $ TransformationType ValueString ValueBool
    go (IsSuffixOf _) = pure $ TransformationType ValueString ValueBool
    go (IsInfixOf _) = pure $ TransformationType ValueString ValueBool
    go IsEmpty = pure $ TransformationType ValueArray ValueBool
    go ArrayLength = pure $ TransformationType ValueArray ValueNumber
    go ArrayReverse = pure $ TransformationType ValueArray ValueArray
    go ArrayUnique = pure $ TransformationType ValueArray ValueArray
    go (Merge _) = pure $ TransformationType ValueObject ValueObject
    go (DeepMerge _) = pure $ TransformationType ValueObject ValueObject
    go ArraySort = pure $ TransformationType ValueArray ValueArray
    go (Equal _) = pure $ TransformationType ValueAny ValueBool
    go (Const v) = pure $ TransformationType ValueAny (valueType v)
    go Not = pure $ TransformationType ValueBool ValueBool
    go (Or left right) = boolPair Or InvalidOr left right
    go (And left right) = boolPair And InvalidAnd left right
    go (Xor left right) = boolPair Xor InvalidXor left right
    go (Compose left right) = do
      lt <- go left
      rt <- go right
      let conditions =
            [ transformationInput lt == ValueAny,
              transformationOutput rt == transformationInput lt
            ]
          inputType = transformationInput rt
          outputType = transformationOutput lt
      if getAny $ foldMap Any conditions
        then pure $ TransformationType inputType outputType
        else Left $ InvalidCompose (Compose left right) (transformationOutput rt) (transformationInput lt)

-- | Check a boolean connective: both branches must produce booleans;
-- the input type comes from the left branch.
boolPair ::
  (Transformation -> Transformation -> Transformation) ->
  (Transformation -> ValueType -> TransformationTypeError) ->
  Transformation ->
  Transformation ->
  Either TransformationTypeError TransformationType
boolPair ctor toErr left right = do
  lt <- inferTransformationType left
  rt <- inferTransformationType right
  case (transformationOutput lt, transformationOutput rt) of
    (ValueBool, ValueBool) ->
      pure $ TransformationType (transformationInput lt) ValueBool
    (ValueBool, other) ->
      Left $ toErr (ctor left right) other
    (other, _) ->
      Left $ toErr (ctor left right) other

-- | The 'ValueType' of an embedded JSON literal.
valueType :: Value -> ValueType
valueType (Object _) = ValueObject
valueType (Array _) = ValueArray
valueType (String _) = ValueString
valueType (Number _) = ValueNumber
valueType (Bool _) = ValueBool
valueType Null = ValueNull