packages feed

wireform-proto-0.2.0.0: src/Proto/IDL/Options.hs

{- | Standard protobuf option extraction and language-specific attribute support.

Protobuf defines a set of standard options recognized by all conformant
implementations. This module provides structured access to these options,
plus Haskell-specific mappings.

Standard file-level options:

* @java_package@ — Java package name
* @java_outer_classname@ — Java outer class
* @java_multiple_files@ — Generate one .java per message
* @go_package@ — Go import path
* @csharp_namespace@ — C# namespace
* @objc_class_prefix@ — Objective-C class prefix
* @php_namespace@ — PHP namespace
* @ruby_package@ — Ruby module name
* @swift_prefix@ — Swift type prefix
* @optimize_for@ — SPEED, CODE_SIZE, or LITE_RUNTIME
* @cc_enable_arenas@ — C++ arena allocation
* @deprecated@ — Mark entire file deprecated

Standard message/field/enum options:

* @deprecated@ — Generate deprecation warnings
* @packed@ — Use packed wire encoding for repeated scalars
* @json_name@ — Override the JSON field name
* @map_entry@ — Mark a message as a synthetic map entry
* @allow_alias@ — Allow multiple enum values with the same number
-}
module Proto.IDL.Options (
  -- * File-level options
  FileOptions (..),
  extractFileOptions,

  -- * Message-level options
  MessageOptions (..),
  extractMessageOptions,

  -- * Field-level options
  FieldOptions (..),
  extractFieldOptions,

  -- * Enum-level options
  EnumOptions (..),
  extractEnumOptions,

  -- * Enum value options
  EnumValueOptions (..),
  extractEnumValueOptions,

  -- * Service/RPC options
  ServiceOptions (..),
  extractServiceOptions,
  RpcOptions (..),
  extractRpcOptions,

  -- * Optimization level
  OptimizeMode (..),

  -- * Cross-language package mapping
  LanguagePackages (..),
  extractLanguagePackages,

  -- * Deprecation
  isDeprecated,
  deprecatedFields,
  deprecatedEnumValues,
) where

import Data.Char (isAsciiLower)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Proto.IDL.AST
import Proto.IDL.Annotations


-- | Standard file-level options.
data FileOptions = FileOptions
  { foJavaPackage :: !(Maybe Text)
  -- ^ @java_package@ option.
  , foJavaOuterClassname :: !(Maybe Text)
  -- ^ @java_outer_classname@ option.
  , foJavaMultipleFiles :: !Bool
  -- ^ @java_multiple_files@ option.
  , foGoPackage :: !(Maybe Text)
  -- ^ @go_package@ option.
  , foCsharpNamespace :: !(Maybe Text)
  -- ^ @csharp_namespace@ option.
  , foObjcClassPrefix :: !(Maybe Text)
  -- ^ @objc_class_prefix@ option.
  , foPhpNamespace :: !(Maybe Text)
  -- ^ @php_namespace@ option.
  , foRubyPackage :: !(Maybe Text)
  -- ^ @ruby_package@ option.
  , foSwiftPrefix :: !(Maybe Text)
  -- ^ @swift_prefix@ option.
  , foOptimizeFor :: !OptimizeMode
  -- ^ @optimize_for@ option.
  , foCcEnableArenas :: !Bool
  -- ^ @cc_enable_arenas@ option.
  , foDeprecated :: !Bool
  -- ^ Whether the file is marked deprecated.
  }
  deriving stock (Show, Eq)


-- | The @optimize_for@ file option.
data OptimizeMode
  = -- | Optimize for speed (default).
    Speed
  | -- | Optimize for code size.
    CodeSize
  | -- | Use the lite runtime.
    LiteRuntime
  deriving stock (Show, Eq, Ord)


-- | Extract standard file-level options from a list of option definitions.
extractFileOptions :: [OptionDef] -> FileOptions
extractFileOptions opts =
  FileOptions
    { foJavaPackage = lookupSimpleOption "java_package" opts >>= optionAsString
    , foJavaOuterClassname = lookupSimpleOption "java_outer_classname" opts >>= optionAsString
    , foJavaMultipleFiles = fromMaybe False (lookupSimpleOption "java_multiple_files" opts >>= optionAsBool)
    , foGoPackage = lookupSimpleOption "go_package" opts >>= optionAsString
    , foCsharpNamespace = lookupSimpleOption "csharp_namespace" opts >>= optionAsString
    , foObjcClassPrefix = lookupSimpleOption "objc_class_prefix" opts >>= optionAsString
    , foPhpNamespace = lookupSimpleOption "php_namespace" opts >>= optionAsString
    , foRubyPackage = lookupSimpleOption "ruby_package" opts >>= optionAsString
    , foSwiftPrefix = lookupSimpleOption "swift_prefix" opts >>= optionAsString
    , foOptimizeFor = parseOptimizeMode (lookupSimpleOption "optimize_for" opts >>= optionAsIdent)
    , foCcEnableArenas = fromMaybe False (lookupSimpleOption "cc_enable_arenas" opts >>= optionAsBool)
    , foDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    }


parseOptimizeMode :: Maybe Text -> OptimizeMode
parseOptimizeMode (Just "CODE_SIZE") = CodeSize
parseOptimizeMode (Just "LITE_RUNTIME") = LiteRuntime
parseOptimizeMode _ = Speed


-- | Standard message-level options.
data MessageOptions = MessageOptions
  { moDeprecated :: !Bool
  -- ^ Whether the message is marked deprecated.
  , moMapEntry :: !Bool
  -- ^ Whether this message is a synthetic map entry type.
  }
  deriving stock (Show, Eq)


-- | Extract standard message-level options from a list of option definitions.
extractMessageOptions :: [OptionDef] -> MessageOptions
extractMessageOptions opts =
  MessageOptions
    { moDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    , moMapEntry = fromMaybe False (lookupSimpleOption "map_entry" opts >>= optionAsBool)
    }


-- | Standard field-level options.
data FieldOptions = FieldOptions
  { fldDeprecated :: !Bool
  -- ^ Whether the field is marked deprecated.
  , fldPacked :: !(Maybe Bool)
  -- ^ The @packed@ option value, if set.
  , fldJsonName :: !(Maybe Text)
  -- ^ The @json_name@ override, if set.
  }
  deriving stock (Show, Eq)


-- | Extract standard field-level options from a list of option definitions.
extractFieldOptions :: [OptionDef] -> FieldOptions
extractFieldOptions opts =
  FieldOptions
    { fldDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    , fldPacked = lookupSimpleOption "packed" opts >>= optionAsBool
    , fldJsonName = lookupSimpleOption "json_name" opts >>= optionAsString
    }


-- | Standard enum-level options.
data EnumOptions = EnumOptions
  { eoAllowAlias :: !Bool
  -- ^ Whether multiple enum values may share the same number.
  , eoDeprecated :: !Bool
  -- ^ Whether the enum is marked deprecated.
  }
  deriving stock (Show, Eq)


-- | Extract standard enum-level options from a list of option definitions.
extractEnumOptions :: [OptionDef] -> EnumOptions
extractEnumOptions opts =
  EnumOptions
    { eoAllowAlias = fromMaybe False (lookupSimpleOption "allow_alias" opts >>= optionAsBool)
    , eoDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    }


-- | Standard enum value options.
newtype EnumValueOptions = EnumValueOptions
  { evoDeprecated :: Bool
  }
  deriving stock (Show, Eq)


-- | Extract standard enum value options from a list of option definitions.
extractEnumValueOptions :: [OptionDef] -> EnumValueOptions
extractEnumValueOptions opts =
  EnumValueOptions
    { evoDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    }


-- | Standard service-level options.
newtype ServiceOptions = ServiceOptions
  { svoDeprecated :: Bool
  }
  deriving stock (Show, Eq)


-- | Extract standard service-level options from a list of option definitions.
extractServiceOptions :: [OptionDef] -> ServiceOptions
extractServiceOptions opts =
  ServiceOptions
    { svoDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    }


-- | Standard RPC-level options.
newtype RpcOptions = RpcOptions
  { roDeprecated :: Bool
  }
  deriving stock (Show, Eq)


-- | Extract standard RPC-level options from a list of option definitions.
extractRpcOptions :: [OptionDef] -> RpcOptions
extractRpcOptions opts =
  RpcOptions
    { roDeprecated = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)
    }


-- | Cross-language package mapping extracted from a proto file's options.
data LanguagePackages = LanguagePackages
  { lpProtoPackage :: !(Maybe Text)
  , lpJavaPackage :: !(Maybe Text)
  , lpGoPackage :: !(Maybe Text)
  , lpCsharpNamespace :: !(Maybe Text)
  , lpPhpNamespace :: !(Maybe Text)
  , lpRubyPackage :: !(Maybe Text)
  , lpSwiftPrefix :: !(Maybe Text)
  , lpObjcPrefix :: !(Maybe Text)
  , lpHaskellModule :: !Text
  }
  deriving stock (Show, Eq)


{- | Extract the cross-language package mapping from a proto file.
The Haskell module name is derived from the proto package.
-}
extractLanguagePackages :: ProtoFile -> LanguagePackages
extractLanguagePackages pf =
  let fo = extractFileOptions (protoOptions pf)
  in LanguagePackages
       { lpProtoPackage = protoPackage pf
       , lpJavaPackage = foJavaPackage fo
       , lpGoPackage = foGoPackage fo
       , lpCsharpNamespace = foCsharpNamespace fo
       , lpPhpNamespace = foPhpNamespace fo
       , lpRubyPackage = foRubyPackage fo
       , lpSwiftPrefix = foSwiftPrefix fo
       , lpObjcPrefix = foObjcClassPrefix fo
       , lpHaskellModule = deriveHaskellModule (protoPackage pf)
       }


deriveHaskellModule :: Maybe Text -> Text
deriveHaskellModule Nothing = "Generated"
deriveHaskellModule (Just pkg) =
  T.intercalate "." (fmap capitalize (T.splitOn "." pkg))
  where
    capitalize t = case T.uncons t of
      Just (c, rest) -> T.cons (toUpper c) rest
      Nothing -> t
    toUpper c
      | isAsciiLower c = toEnum (fromEnum c - 32)
      | otherwise = c


-- | Check if something is marked deprecated.
isDeprecated :: [OptionDef] -> Bool
isDeprecated opts = fromMaybe False (lookupSimpleOption "deprecated" opts >>= optionAsBool)


-- | All deprecated fields in a message.
deprecatedFields :: MessageDef -> [FieldDef]
deprecatedFields msg =
  concatMap (filter (isDeprecated . fieldOptions) . extractF) (msgElements msg)
  where
    extractF (MEField fd) = [fd]
    extractF _ = []


-- | All deprecated enum values.
deprecatedEnumValues :: EnumDef -> [EnumValue]
deprecatedEnumValues ed = filter (isDeprecated . evOptions) (enumValues ed)