packages feed

wireform-derive-0.1.0.0: src/Wireform/Derive/Modifier.hs

{- | Annotation vocabulary for customising wireform-driven instance
generation.

A 'Modifier' is a single customisation directive (\"rename this
field\", \"flatten this nested record\", \"this field is optional in
Thrift\", ...). Multiple modifiers compose as a 'Modifiers' value.

Modifiers are attached to types and fields using GHC @ANN@ pragmas:

@
data Person = Person
  { personName :: !Text   -- ^ wire key: \"name\"
  , personAge  :: !Int    -- ^ wire key: \"age\"
  } deriving stock Generic

{\-\# ANN type Person (rename "person") \#-\}
{\-\# ANN personName  (rename "name") \#-\}
{\-\# ANN personAge   (renameStyle SnakeCase) \#-\}
@

Per-format derivers (@deriveProto@, @deriveCBOR@, @deriveAeson@, ...)
read these annotations via 'Wireform.Derive.ModifierInfo.reifyModifierInfoFor'
and shape the generated code accordingly.
-}
module Wireform.Derive.Modifier (
  -- * Modifiers
  Modifier (..),
  Modifiers (..),
  noModifiers,

  -- * Renames
  Rename (..),
  rename,
  renameStyle,
  renameWith,
  renameIdiomatic,

  -- * Wire-format overrides
  WireOverride (..),

  -- * Map keys (proto-flavoured)
  MapKeyScalar (..),

  -- * Smart constructors (general)
  coerced,
  flatten,
  skip,
  defaults,
  tag,
  required,
  optional,
  wireOverride,
  customModifier,
  mapKey,
  oneof,

  -- * Per-backend targeting
  forBackend,
  forBackends,
  disableFor,
  onlyFor,

  -- * Inspection
  modifierBackends,
  modifierIsBackendScoped,
) where

import Data.Data (Data)
import Data.Text (Text)
import GHC.Generics (Generic)
import Language.Haskell.TH.Syntax (Name)
import Wireform.Derive.Backend
import Wireform.Derive.NameStyle


-- ---------------------------------------------------------------------------
-- Rename
-- ---------------------------------------------------------------------------

-- | How to compute a wire key from a Haskell field's selector base name.
data Rename
  = -- | Use this exact 'Text' as the wire key.
    RenameTo !Text
  | -- | Apply a 'NameStyle' transformation. Evaluated entirely at
    -- splice time, so the result is baked into the generated code as
    -- a literal 'Text'.
    RenameStyle !NameStyle
  | -- | Apply the named @Text -> Text@ function at runtime. Use only
    -- when 'NameStyle' cannot express the desired transformation.
    RenameFn !Name
  deriving stock (Eq, Ord, Show, Data, Generic)


-- ---------------------------------------------------------------------------
-- WireOverride
-- ---------------------------------------------------------------------------

-- | Force a non-default wire encoding for a numeric or string field.
data WireOverride
  = -- | Use ZigZag encoding for signed integers (proto's @sint32@ /
    -- @sint64@).
    WireZigZag
  | -- | Use a fixed-width little-endian encoding (proto's @fixed32@
    -- / @fixed64@ / @sfixed32@ / @sfixed64@).
    WireFixed
  | -- | Pack a repeated scalar field (proto3 default for scalars).
    WirePacked
  | -- | Encode this field as a UTF-8 'Text' regardless of the inferred
    -- representation.
    WireString
  | -- | Encode this field as raw bytes regardless of the inferred
    -- representation.
    WireBytes
  deriving stock (Eq, Ord, Show, Data, Generic)


-- ---------------------------------------------------------------------------
-- MapKeyScalar
-- ---------------------------------------------------------------------------

{- | Scalar types that protobuf permits as map keys (proto2 §maps,
proto3 §maps). Excludes @float@, @double@, @bytes@, message and
enum types.

Lives in @wireform-derive@ rather than @wireform-proto@ so that
annotations can reference proto map-key types without forcing
non-proto packages to depend on proto. The proto deriver projects
this onto its own @Proto.IDL.AST.ScalarType@.
-}
data MapKeyScalar
  = MapKeyInt32
  | MapKeyInt64
  | MapKeyUInt32
  | MapKeyUInt64
  | MapKeySInt32
  | MapKeySInt64
  | MapKeyFixed32
  | MapKeyFixed64
  | MapKeySFixed32
  | MapKeySFixed64
  | MapKeyBool
  | MapKeyString
  deriving stock (Eq, Ord, Show, Data, Generic, Enum, Bounded)


-- ---------------------------------------------------------------------------
-- Modifier
-- ---------------------------------------------------------------------------

{- | A single annotation directive. Each format deriver consults
modifiers it understands and silently ignores the rest, so a single
annotation can simultaneously inform several backends.
-}
data Modifier
  = -- | Rename a field on the wire.
    ModRename !Rename
  | -- | Encode / decode the field via the named @newtype@-style
    -- coercion target.
    ModCoerce !Name
  | -- | Inline the contents of this nested record into its parent's
    -- field set, rather than nesting it as a sub-message.
    ModFlatten
  | -- | Omit this field from the wire encoding entirely (decoders
    -- supply 'mempty' / @def@).
    ModSkip
  | -- | Use the named function as the field's default when decoding
    -- a missing value.
    ModDefaults !Name
  | -- | Manually fix this field's numeric tag / id (proto field
    -- number, Thrift field id).
    ModTag !Int
  | -- | Mark a field required (Thrift / proto2 semantics).
    ModRequired
  | -- | Mark a field optional.
    ModOptional
  | -- | Override the field's wire encoding (see 'WireOverride').
    ModWireOverride !WireOverride
  | -- | Apply the inner modifiers /only/ for the listed backends.
    ModForBackends ![Backend] ![Modifier]
  | -- | Apply this single modifier /only/ for the listed backends.
    -- Equivalent to @ModForBackends bs [m]@; provided for cheaper
    -- pretty-printing and tighter @ANN@ payloads in common cases.
    ModBackendOnly ![Backend] !Modifier
  | -- | Skip this name entirely for the listed backends. Equivalent to
    -- @ModForBackends bs [ModSkip]@; provided as its own constructor
    -- so per-format derivers can short-circuit cheaply.
    ModBackendDisable ![Backend]
  | -- | Backend-specific opaque payload. Tagged by an arbitrary
    -- 'Text' identifier so backends can recognise their own; the
    -- payload itself is a 'String' rather than a 'ByteString'
    -- because GHC's 'ANN' machinery serialises the modifier via
    -- @Data.Data@, which works for any algebraic type but fails on
    -- the sealed 'ByteString' instance.
    ModCustom !Text !String
  | -- | This @HashMap@ / @Map@ field is encoded as a proto3 @map@.
    -- The 'MapKeyScalar' picks the wire encoding for the key half.
    -- Value type is inferred from the field's Haskell type.
    --
    -- Only consulted by the proto deriver; other backends ignore it.
    ModMapKey !MapKeyScalar
  | -- | Group this constructor (in a sum) or this field (in a
    -- record) into the named proto @oneof@. All fields sharing the
    -- same group name encode under one @oneof@ block.
    --
    -- Only consulted by the proto deriver; other backends ignore it.
    ModOneof !Text
  deriving stock (Eq, Ord, Show, Data, Generic)


-- ---------------------------------------------------------------------------
-- Modifiers (composition)
-- ---------------------------------------------------------------------------

{- | A composed bundle of modifiers. Newtype around @[Modifier]@ with a
right-biased 'Semigroup' (later writes shadow earlier ones, matching
the order in which @ANN@ pragmas are reified).
-}
newtype Modifiers = Modifiers {getModifiers :: [Modifier]}
  deriving stock (Eq, Ord, Show, Data, Generic)


instance Semigroup Modifiers where
  Modifiers xs <> Modifiers ys = Modifiers (xs <> ys)


instance Monoid Modifiers where
  mempty = Modifiers []


-- | Empty modifier bundle.
noModifiers :: Modifiers
noModifiers = mempty


-- ---------------------------------------------------------------------------
-- Smart constructors: renames
-- ---------------------------------------------------------------------------

-- | Rename a field to a fixed string.
rename :: Text -> Modifier
rename = ModRename . RenameTo


{- | Apply a 'NameStyle' transformation. The deriver evaluates this at
splice time.
-}
renameStyle :: NameStyle -> Modifier
renameStyle = ModRename . RenameStyle


{- | Apply the named @Text -> Text@ function at runtime. Pass the
function's name with TH's @\'@ syntax, e.g. @renameWith \'myRenamer@.
-}
renameWith :: Name -> Modifier
renameWith = ModRename . RenameFn


{- | Use the active backend's idiomatic naming convention. Equivalent to
@renameStyle Idiomatic@; provided as a smart constructor for
discoverability.
-}
renameIdiomatic :: Modifier
renameIdiomatic = renameStyle Idiomatic


-- ---------------------------------------------------------------------------
-- Smart constructors: general
-- ---------------------------------------------------------------------------

-- | See 'ModCoerce'.
coerced :: Name -> Modifier
coerced = ModCoerce


-- | See 'ModFlatten'.
flatten :: Modifier
flatten = ModFlatten


-- | See 'ModSkip'.
skip :: Modifier
skip = ModSkip


-- | See 'ModDefaults'.
defaults :: Name -> Modifier
defaults = ModDefaults


-- | See 'ModTag'.
tag :: Int -> Modifier
tag = ModTag


-- | See 'ModRequired'.
required :: Modifier
required = ModRequired


-- | See 'ModOptional'.
optional :: Modifier
optional = ModOptional


-- | See 'ModWireOverride'.
wireOverride :: WireOverride -> Modifier
wireOverride = ModWireOverride


{- | Embed an opaque backend-specific payload. The 'Text' tag should be
a fully-qualified, namespaced identifier (e.g.
@"wireform-proto.json-name"@) so different backends do not clash on
the same tag.

The payload is typed as 'String' rather than 'ByteString' because
GHC's 'ANN' machinery serialises modifiers via @Data.Data@, and
'ByteString' has a sealed 'Data' instance that fails the round
trip. For typed payloads, prefer
'Wireform.Derive.Extension.extension'.
-}
customModifier :: Text -> String -> Modifier
customModifier = ModCustom


{- | Mark a @HashMap@ / @Map@ field as a proto @map<K,V>@ with the
supplied key encoding. See 'ModMapKey'.
-}
mapKey :: MapKeyScalar -> Modifier
mapKey = ModMapKey


-- | Group this name into the named proto @oneof@. See 'ModOneof'.
oneof :: Text -> Modifier
oneof = ModOneof


-- ---------------------------------------------------------------------------
-- Smart constructors: per-backend targeting
-- ---------------------------------------------------------------------------

{- | Restrict a single modifier to one backend.

@forBackend backendProto (rename "name")@
-}
forBackend :: Backend -> Modifier -> Modifier
forBackend b = ModBackendOnly [b]


{- | Restrict a list of modifiers to a list of backends.

@forBackends [backendProto, backendJSON] [rename "name", tag 7]@
-}
forBackends :: [Backend] -> [Modifier] -> Modifier
forBackends = ModForBackends


-- | Restrict a single modifier to several backends.
onlyFor :: [Backend] -> Modifier -> Modifier
onlyFor = ModBackendOnly


-- | Skip this name entirely for the listed backends.
disableFor :: [Backend] -> Modifier
disableFor = ModBackendDisable


-- ---------------------------------------------------------------------------
-- Inspection
-- ---------------------------------------------------------------------------

{- | The set of backends a modifier targets, or 'Nothing' for a global
modifier (one that applies to every backend).
-}
modifierBackends :: Modifier -> Maybe [Backend]
modifierBackends = \case
  ModForBackends bs _ -> Just bs
  ModBackendOnly bs _ -> Just bs
  ModBackendDisable bs -> Just bs
  _ -> Nothing


-- | True iff the modifier is restricted to a specific set of backends.
modifierIsBackendScoped :: Modifier -> Bool
modifierIsBackendScoped m = case modifierBackends m of
  Just _ -> True
  Nothing -> False