packages feed

sqlc-hs-0.2.0.0: src/Sqlc/Hs/Config.hs

module Sqlc.Hs.Config
  ( Config (..),
    Override (..),
    HaskellType (..),
    Naming (..),
    parseConfig,
    defaultConfig,
  )
where

import Data.Aeson
  ( FromJSON (..),
    eitherDecodeStrict,
    withObject,
    (.!=),
    (.:),
    (.:?),
  )
import Data.Vector (Vector)

-- | Serves as both, global and per query configuration. Multiple 'Config's can be
-- merged through the use of the 'Semigroup' instance.
data Config = Config
  { -- | If requested, a cabal file with this package name will be generated.
    cabalPackageName :: Maybe Text,
    -- | Package version to be used with the cabal package.
    cabalPackageVersion :: Maybe Text,
    -- | Default extensions that will be placed in the default-extension stanza
    cabalDefaultExtensions :: [Text],
    -- | Module prefix, e.g. Data.Queries
    haskellModulePrefix :: Maybe Text,
    -- | List of lists of overrides. Nesting is list of allowing cheap
    -- concatenation. No need to create a one big vector.
    overrides :: [Vector Override],
    -- | Templates for the names of generated declarations.
    naming :: Naming
  }

instance Semigroup Config where
  config1 <> config2 =
    Config
      { cabalPackageName =
          getFirst $ First config1.cabalPackageName <> First config2.cabalPackageName,
        cabalPackageVersion =
          getFirst $ First config1.cabalPackageVersion <> First config2.cabalPackageVersion,
        haskellModulePrefix =
          getFirst $ First config1.haskellModulePrefix <> First config2.haskellModulePrefix,
        cabalDefaultExtensions =
          config1.cabalDefaultExtensions <> config2.cabalDefaultExtensions,
        overrides =
          config1.overrides <> config2.overrides,
        naming =
          config1.naming <> config2.naming
      }

instance Monoid Config where
  mempty =
    Config
      { overrides = [],
        haskellModulePrefix = Nothing,
        cabalPackageName = Nothing,
        cabalPackageVersion = Nothing,
        cabalDefaultExtensions = [],
        naming = mempty
      }

instance FromJSON Config where
  parseJSON = withObject "Config" $ \o ->
    Config
      <$> o .:? "cabal_package_name"
      <*> o .:? "cabal_package_version"
      <*> o .:? "cabal_default_extensions" .!= []
      <*> o .:? "haskell_module_prefix"
      <*> fmap pure (o .:? "overrides" .!= mempty)
      <*> o .:? "naming" .!= mempty

-- | Mustache-style templates for the names of generated declarations. Every
-- field is optional; an omitted template falls back to the default that
-- reproduces sqlc-hs's historical naming, so existing configurations generate
-- byte-identical code.
--
-- See the README for the variables available to each template.
data Naming = Naming
  { -- | Query declaration, default @query_{{query}}@. Context: @query@.
    query :: Maybe Text,
    -- | Params constructor, default @Params_{{query}}@. Context: @query@.
    paramsConstructor :: Maybe Text,
    -- | Result constructor, default @Result_{{query}}@. Context: @query@.
    resultConstructor :: Maybe Text,
    -- | Enum value constructor, default @Enum_{{enum}}_{{value}}@. Context:
    -- @enum@ (the database type name), @value@ (the enum value).
    enumConstructor :: Maybe Text,
    -- | Record field, default @{{prefix}}{{column}}@. Context: @column@,
    -- @table@, @table_alias@, @schema@ and @prefix@ (the historical
    -- namespacing: the table alias or table name followed by @_@, empty for
    -- table-less expression outputs).
    field :: Maybe Text
  }

instance Semigroup Naming where
  naming1 <> naming2 =
    Naming
      { query =
          getFirst $ First naming1.query <> First naming2.query,
        paramsConstructor =
          getFirst $ First naming1.paramsConstructor <> First naming2.paramsConstructor,
        resultConstructor =
          getFirst $ First naming1.resultConstructor <> First naming2.resultConstructor,
        enumConstructor =
          getFirst $ First naming1.enumConstructor <> First naming2.enumConstructor,
        field =
          getFirst $ First naming1.field <> First naming2.field
      }

instance Monoid Naming where
  mempty =
    Naming
      { query = Nothing,
        paramsConstructor = Nothing,
        resultConstructor = Nothing,
        enumConstructor = Nothing,
        field = Nothing
      }

instance FromJSON Naming where
  parseJSON = withObject "Naming" $ \o ->
    Naming
      <$> o .:? "query"
      <*> o .:? "params_constructor"
      <*> o .:? "result_constructor"
      <*> o .:? "enum_constructor"
      <*> o .:? "field"

data Override = Override
  { haskellType :: NonEmpty HaskellType,
    -- | Database type to match, e.g. @text@. Optional when 'column' is given.
    databaseType :: Maybe Text,
    -- | Column to match: @column@, @table.column@ or @schema.table.column@.
    -- A bare @column@ also matches aliased expression outputs, which carry no
    -- table. Optional when 'databaseType' is given; when both are given, both
    -- must match.
    column :: Maybe Text,
    -- | For global overrides only when two different engines are in use.
    engine :: Maybe Text,
    -- | True if the haskellType should override if the matching type is nullable
    nullable :: Maybe Bool
  }

instance FromJSON Override where
  parseJSON = withObject "Override" $ \o -> do
    override <-
      Override
        <$> ( -- We allow multiple haskell-types per database type
              -- to support composite types and their dependencies.
              fmap pure (o .: "haskell_type")
                <|> o
                .: "haskell_type"
            )
        <*> o .:? "db_type"
        <*> o .:? "column"
        <*> o .:? "engine"
        <*> o .:? "nullable"
    when (isNothing override.databaseType && isNothing override.column) $
      fail "override requires at least one of \"db_type\" or \"column\""
    pure override

-- | A haskell type denotes a fully qualified data type with module
-- and import and all.
--
-- Haskellers hate this definition as it's very indefinite. Here's some
-- perspective:
--
-- We force people to fully qualify (<=> all fields "Just") when specifying
-- overrides. No indefiniteness here. Check `FromJSON HaskellType`.
--
-- Internally we refer to fully specified types as well, exclusively.
-- No indefiniteness here either.
--
-- But: When resolving types we want to be able to express composed types
-- like "Maybe (Vector Text)". We do not want to specify module and package
-- but really only the name.
data HaskellType = HaskellType
  { package :: Maybe Text,
    module' :: Maybe Text,
    name :: Maybe Text
  }

instance FromJSON HaskellType where
  parseJSON = withObject "HaskellType" $ \o ->
    HaskellType
      <$> o .:? "package"
      <*> o .:? "module"
      <*> o .:? "type"

parseConfig :: ByteString -> Either String Config
parseConfig input =
  case input of
    "" -> Right mempty
    input -> eitherDecodeStrict input

defaultConfig :: Config
defaultConfig =
  mempty
    { cabalPackageName = Just "queries",
      cabalPackageVersion = Just "0.1.0.0",
      cabalDefaultExtensions = [],
      haskellModulePrefix = Just "Queries",
      overrides = []
    }