packages feed

fcm-client 0.1.0.0 → 0.2.0.0

raw patch · 9 files changed

+194/−193 lines, 9 filesdep ~basedep ~bytestringdep ~textPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, bytestring, text

API changes (from Hackage documentation)

- FCMClient.JSON.Types: fcmRegistrationIDs :: Lens' FCMMessage (Maybe (NonEmpty Text))
- FCMClient.Types: fcmRegistrationIDs :: Lens' FCMMessage (Maybe (NonEmpty Text))
+ FCMClient.JSON.Types: fcmAndroidChannelId :: Lens' FCMNotification (Maybe Text)
+ FCMClient.JSON.Types: fcmRegistrationIds :: Lens' FCMMessage (Maybe (NonEmpty Text))
+ FCMClient.Types: data Scientific
+ FCMClient.Types: decode :: FromJSON a => ByteString -> Maybe a
+ FCMClient.Types: encode :: ToJSON a => a -> ByteString
+ FCMClient.Types: fcmAndroidChannelId :: Lens' FCMNotification (Maybe Text)
+ FCMClient.Types: fcmRegistrationIds :: Lens' FCMMessage (Maybe (NonEmpty Text))
- FCMClient: fcmCallJSON :: (ToJSON req) => ByteString -> req -> IO FCMResult
+ FCMClient: fcmCallJSON :: ToJSON req => ByteString -> req -> IO FCMResult
- FCMClient.Types: fcmBodyLocArgs :: (Applicative f) => ([FCMLocValue] -> f [FCMLocValue]) -> FCMNotification -> f FCMNotification
+ FCMClient.Types: fcmBodyLocArgs :: Applicative f => ([FCMLocValue] -> f [FCMLocValue]) -> FCMNotification -> f FCMNotification
- FCMClient.Types: fcmContentAvailable :: (Applicative f) => (Bool -> f Bool) -> FCMMessage -> f FCMMessage
+ FCMClient.Types: fcmContentAvailable :: Applicative f => (Bool -> f Bool) -> FCMMessage -> f FCMMessage
- FCMClient.Types: fcmDelayWhileIdle :: (Applicative f) => (Bool -> f Bool) -> FCMMessage -> f FCMMessage
+ FCMClient.Types: fcmDelayWhileIdle :: Applicative f => (Bool -> f Bool) -> FCMMessage -> f FCMMessage
- FCMClient.Types: fcmDryRun :: (Applicative f) => (Bool -> f Bool) -> FCMMessage -> f FCMMessage
+ FCMClient.Types: fcmDryRun :: Applicative f => (Bool -> f Bool) -> FCMMessage -> f FCMMessage
- FCMClient.Types: fcmPriority :: (Applicative f) => (FCMPriority -> f FCMPriority) -> FCMMessage -> f FCMMessage
+ FCMClient.Types: fcmPriority :: Applicative f => (FCMPriority -> f FCMPriority) -> FCMMessage -> f FCMMessage
- FCMClient.Types: fcmTitleLocArgs :: (Applicative f) => ([FCMLocValue] -> f [FCMLocValue]) -> FCMNotification -> f FCMNotification
+ FCMClient.Types: fcmTitleLocArgs :: Applicative f => ([FCMLocValue] -> f [FCMLocValue]) -> FCMNotification -> f FCMNotification
- FCMClient.Types: fcmWithNotification :: (Applicative f) => (FCMNotification -> f FCMNotification) -> FCMMessage -> f FCMMessage
+ FCMClient.Types: fcmWithNotification :: Applicative f => (FCMNotification -> f FCMNotification) -> FCMMessage -> f FCMMessage

Files

+ CHANGELOG.md view
@@ -0,0 +1,25 @@+# Changelog++`fcm-client` uses [PVP Versioning][1].+The changelog is available [on GitHub][2].++## 0.2.0.0 — Sep 18, 2019++* [#3](https://github.com/holmusk/fcm-client/issues/3):+  Fix bug serializing the `registration_ids` field.+  (by [@bitc](https://github.com/bitc))+* [#6](https://github.com/holmusk/fcm-client/pull/6):+  Add support for the `android_channel_id` field.+  (by [@bitc](https://github.com/bitc))+* [#7](https://github.com/holmusk/fcm-client/issues/7):+  Support latest 3 major GHC versions (on CI as well).+  (by [@chshersh](https://github.com/chshersh))+* [#4](https://github.com/holmusk/fcm-client/pull/4):+  Add support for GHC-8.2.2.+  (by [@bitc](https://github.com/bitc))+* [#2](https://github.com/Holmusk/fcm-client/pull/2):+  Updated dependencies, code cleanup.+  (by [@andreyk0](https://github.com/andreyk0))++[1]: https://pvp.haskell.org+[2]: https://github.com/holmusk/fcm-client/releases
README.md view
@@ -2,4 +2,5 @@  Simple model/client for [firebase cloud messaging](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages). -[Example](cli/Main.hs)+   * [FCM communication example](cli/Main.hs)+   * [Message composition with lenses example](test/Spec.hs)
cli/CliArgs.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE RecordWildCards #-}- module CliArgs        ( CliArgs (..)        , CliCmd (..)@@ -8,7 +6,9 @@        ) where  import Control.Lens+import Data.Default.Class import Data.List.NonEmpty (nonEmpty)+import Data.Monoid ((<>)) import FCMClient.Types import Options.Applicative import System.Environment@@ -22,13 +22,13 @@                        , cliCmd     :: CliCmd                        } -data CliJsonBatchArgs = CliJsonBatchArgs { cliBatchInput  :: (Maybe FilePath)-                                         , cliBatchOutput :: (Maybe FilePath)+data CliJsonBatchArgs = CliJsonBatchArgs { cliBatchInput  :: Maybe FilePath+                                         , cliBatchOutput :: Maybe FilePath                                          , cliBatchConc   :: Int                                          }  data CliCmd = CliCmdSendJsonBatch CliJsonBatchArgs-            | CliCmdSendMessage (FCMMessage -> FCMMessage)+            | CliCmdSendMessage FCMMessage   parseArgs :: Maybe String -- ^ default auth key from shell env@@ -52,30 +52,31 @@  parseCliJsonBatchArgs :: Parser CliJsonBatchArgs parseCliJsonBatchArgs = CliJsonBatchArgs-  <$> (optional $ strOption+  <$> optional ( strOption         ( long "input"        <> short 'i'        <> help "Batch input file, one JSON object per line (or STDIN)."))-  <*> (optional $ strOption+  <*> optional ( strOption         ( long "output"        <> short 'o'        <> help "Batch input file (or STDOUT)."))-  <*> (option (auto >>= (\c -> if (c < 1) then error "Concurrency must be >= 1" else return c))+  <*> option (auto >>= (\c -> if c < 1 then error "Concurrency must be >= 1" else return c))         ( long "concurrency"        <> short 'c'        <> value 1        <> showDefaultWith show-       <> help "How many HTTP requests to run in concurrently."))+       <> help "How many HTTP requests to run in concurrently.")   parseCliCmdSendMessage :: Parser CliCmd-parseCliCmdSendMessage = CliCmdSendMessage-  <$> (  ( fmap (set fcmTo)+parseCliCmdSendMessage = CliCmdSendMessage <$>+    (+        ( fmap (set fcmTo)                 (optionalText                   ( long "to"                  <> short 't'                  <> help "message to (reg token, notification key or topic)"))-        <|> fmap (set fcmRegistrationIDs)+        <|> fmap (set fcmRegistrationIds)                 (textList                   ( long "registration-id"                  <> short 'r'@@ -87,46 +88,46 @@                  <> help "The message target. Supported condition: Topic, formatted as \"'yourTopic' in topics\". This value is case-insensitive.  Supported operators: &&, ||. Maximum two operators per topic message supported."))          ) -      <..> fmap (set $ fcmCollapseKey)+      <..> fmap (set fcmCollapseKey)                 (optionalText                   ( long "collapse-key"                  <> help "Identifies a group of messages that can be collapsed, so that only the last message gets sent when delivery can be resumed.")) -      <..> fmap (set $ J.fcmPriority)+      <..> fmap (set J.fcmPriority)                 (optionalText                   ( long "priority"                  <> help "Sets the priority of the message. Valid values are 'normal' and 'high'. On iOS, these correspond to APNs priorities 5 and 10.")) -      <..> fmap (set $ fcmContentAvailable)+      <..> fmap (set fcmContentAvailable)                 (switch                   ( long "content-available"                  <> help "On iOS, use this field to represent content-available in the APNS payload. When a notification or message is sent and this is set to true, an inactive client app is awoken. On Android, data messages wake the app by default. On Chrome, currently not supported.")) -      <..> fmap (set $ fcmDelayWhileIdle)+      <..> fmap (set fcmDelayWhileIdle)                 (switch                   ( long "delay-while-idle"                  <> help "When this parameter is set to true, it indicates that the message should not be sent until the device becomes active. The default value is false.")) -      <..> fmap (set $ fcmTimeToLive)+      <..> fmap (set fcmTimeToLive)                 (optional $ option auto                   ( long "time-to-live"                  <> help "This parameter specifies how long (in seconds) the message should be kept in FCM storage if the device is offline. The maximum time to live supported is 4 weeks, and the default value is 4 weeks. For more information, see Setting the lifespan of a message.")) -      <..> fmap (set $ fcmRestrictedPackageName)+      <..> fmap (set fcmRestrictedPackageName)                 (optionalText                   ( long "restricted-package-name"                  <> help "This parameter specifies the package name of the application where the registration tokens must match in order to receive the message.")) -      <..> fmap (set $ fcmDryRun)+      <..> fmap (set fcmDryRun)                 (switch                   ( long "dry-run"                  <> help "This parameter, when set to true, allows developers to test a request without actually sending a message. The default value is false.")) -      <..> fmap (set $ fcmData)-                (fmap (J.decode . LB.pack) (strOption+      <..> fmap (set fcmData . J.decode . LB.pack)+                (strOption                   ( long "data"                  <> short 'd'-                 <> help "This parameter specifies the custom key-value pairs of the message's payload.  For example, with data:{\"score\":\"3x1\"}: On iOS, if the message is sent via APNS, it represents the custom data fields. If it is sent via FCM connection server, it would be represented as key value dictionary in AppDelegate application:didReceiveRemoteNotification:.  On Android, this would result in an intent extra named score with the string value 3x1.  The key should not be a reserved word (\"from\" or any word starting with \"google\" or \"gcm\"). Do not use any of the words defined in this table (such as collapse_key). ONLY values in string types are supported. You have to convert values in objects or other non-string data types (e.g., integers or booleans) to string.")))+                 <> help "This parameter specifies the custom key-value pairs of the message's payload.  For example, with data:{\"score\":\"3x1\"}: On iOS, if the message is sent via APNS, it represents the custom data fields. If it is sent via FCM connection server, it would be represented as key value dictionary in AppDelegate application:didReceiveRemoteNotification:.  On Android, this would result in an intent extra named score with the string value 3x1.  The key should not be a reserved word (\"from\" or any word starting with \"google\" or \"gcm\"). Do not use any of the words defined in this table (such as collapse_key). ONLY values in string types are supported. You have to convert values in objects or other non-string data types (e.g., integers or booleans) to string."))        <..> fmap (set $ fcmWithNotification . fcmTitle)                 (optionalText@@ -140,6 +141,11 @@                  <> short 'b'                  <> help "notification body text")) +      <..> fmap (set $ fcmWithNotification . fcmAndroidChannelId)+                (optionalText+                  ( long "android-channel-id"+                 <> help "Android: The notification's channel id (new in Android O).  The app must create a channel with this channel ID before any notification with this channel ID is received.  If you don't send this channel ID in the request, or if the channel ID provided has not yet been created by the app, FCM uses the channel ID specified in the app manifest."))+       <..> fmap (set $ fcmWithNotification . fcmIcon)                 (optionalText                   ( long "icon"@@ -189,10 +195,11 @@                 (optionalText                   ( long "title-loc-args"                  <> help "IOS: Indicates the string value to replace format specifiers in the title string for localization.Corresponds to \"title-loc-args\" in the APNs payload.  Android: Indicates the string value to replace format specifiers in the title string for localization. For more information, see Formatting strings."))-      )+       <*> pure def+    )    where optionalText opt = fmap (fmap T.pack) $ optional $ strOption opt-        textList opt = fmap nonEmpty $ many $ fmap T.pack $ strOption opt+        textList opt = fmap nonEmpty $ many $ T.pack <$> strOption opt   -- | Chains functions through Applicative@@ -209,8 +216,8 @@   maybeAuthKey <- lookupEnv "FCM_AUTH_KEY"    let opts = info (helper <*> parseArgs maybeAuthKey) ( fullDesc-               <> progDesc "Simple FCM CLI client, send a test message or a JSON batch from a file."                <> header "Simple FCM CLI client, send a test message or a JSON batch from a file."+               <> progDesc ("E.g. " <> " fcm-client -k AUTH_KEY message --title Test --to /topics/mytopic --color '#FF0000' -d test")                )    execParser opts >>= rwa
cli/Main.hs view
@@ -13,7 +13,7 @@ import Data.Aeson import Data.Conduit import Data.Conduit.Async-import Data.Default.Class+import Data.Monoid ((<>)) import FCMClient import FCMClient.Types import System.IO@@ -26,16 +26,19 @@ import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.List as CL ++-- | Example of sending an individual notification+--   or a batch of pre-formatted JSON notifications+--   See CliArgs.hs for an example of payload construction with lenses main :: IO () main = runWithArgs $ \CliArgs{..} -> do -  let sendMessage msgMod = do-        let msg = msgMod def+  let sendMessage msg = do         putStrLn $ (LUTF8.toString . encode) msg         res <- fcmCallJSON (UTF8.fromString cliAuthKey) msg         case res           of FCMResultSuccess b -> putStrLn $ (LUTF8.toString . encode) b-             FCMResultError   e -> putStrLn $ show e+             FCMResultError   e -> print e        sendMessageBatch CliJsonBatchArgs{..} = do         let buf c = buffer' cliBatchConc c@@ -46,9 +49,8 @@           `buf`           (encodeOutputConduit .| batchOutputConduit cliBatchOutput) -   case cliCmd-    of CliCmdSendMessage msgMod  -> sendMessage msgMod+    of CliCmdSendMessage msg     -> sendMessage msg        CliCmdSendJsonBatch bargs -> runResourceT $ runCConduit $ sendMessageBatch bargs  @@ -60,7 +62,7 @@ -- an FCM request but original input will be propagated to the output, this allows for addition -- of request tracking/debugging fields that makes it easier to interpret results. parseInputConduit :: (MonadIO m)-                  => Conduit BS.ByteString m (Either (BS.ByteString, String) (Value, FCMMessage))+                  => ConduitT BS.ByteString (Either (BS.ByteString, String) (Value, FCMMessage)) m () parseInputConduit = CB.lines .| CL.map (\line -> do   jObj <- case eitherDecode' $ LBS.fromStrict line             of Right v -> Right v@@ -72,31 +74,30 @@   encodeOutputConduit :: (MonadIO m)-                    => Conduit Value m BS.ByteString+                    => ConduitT Value BS.ByteString m () encodeOutputConduit =   CL.map (LBS.toStrict . encode)-    =$= ( awaitForever $ \l -> do yield l-                                  yield "\n" )+    .| awaitForever (\l -> yield l >> yield "\n")   -- | Convert each input line into a JSON object containing original input and results of the call. callFCMConduit :: (MonadIO m, MonadResource m)                => BS.ByteString -- ^ authorization key-               -> Conduit (Either (BS.ByteString,String) (Value, FCMMessage)) m (A.Async Value)+               -> ConduitT (Either (BS.ByteString,String) (Value, FCMMessage)) (A.Async Value) m () callFCMConduit authKey = CL.mapM $ \input -> liftIO . A.async $   case input     of Left (i,e)    -> return $ object [ ("type", "ParserError")                                         , ("error", toJSON e)                                         , ("input", toJSON (UTF8.toString i))                                         ]-       Right (jm, m) -> fmap (resToVal jm) $ retrying retPolicy (const $ shouldRetry) (const $ fcmCallJSON authKey m)+       Right (jm, m) -> resToVal jm <$> retrying retPolicy (const shouldRetry) (const $ fcmCallJSON authKey m)    where retPolicy = constantDelay 1000000 <> limitRetries 5          shouldRetry (FCMResultSuccess _) = return False          shouldRetry (FCMResultError e) = do-          liftIO $ hPutStrLn stderr $ "Client error: " <> (show e)+          liftIO $ hPutStrLn stderr $ "Client error: " <> show e           return $ case e                      of FCMServerError _ _   -> True                         FCMClientHTTPError _ -> True@@ -116,10 +117,10 @@  runInParallel :: (MonadIO m)               => Int -- ^ level-              -> Conduit (A.Async a) m a+              -> ConduitT (A.Async a) a m () runInParallel n = parC []   where parC !xs = do-          let moreCnt = n - (length xs)+          let moreCnt = n - length xs           moreXs <- CL.take moreCnt           let xs' = xs ++ moreXs           if null xs'@@ -129,10 +130,9 @@                   parC $ filter (/= a) xs'  - batchInputConduit :: (MonadResource m)                   => Maybe FilePath-                  -> Producer m BS.ByteString+                  -> ConduitT () BS.ByteString m () batchInputConduit (Just fp) = CB.sourceFile fp batchInputConduit Nothing = do   liftIO $ do hSetBinaryMode stdin True@@ -142,7 +142,7 @@  batchOutputConduit :: (MonadResource m)                   => Maybe FilePath-                  -> Consumer BS.ByteString m ()+                  -> ConduitT BS.ByteString Void m () batchOutputConduit (Just fp) = CB.sinkFile fp batchOutputConduit Nothing = do   liftIO $ do hSetBinaryMode stdout True
fcm-client.cabal view
@@ -1,33 +1,48 @@+cabal-version:       2.4 name:                fcm-client-version:             0.1.0.0+version:             0.2.0.0 synopsis:            Admin API for Firebase Cloud Messaging-description:         Please see README.md+description:         This package provides data type model and functions to call FCM endpoints. homepage:            https://github.com/holmusk/fcm-client#readme-license:             BSD3+license:             BSD-3-Clause license-file:        LICENSE author:              Andrey Kartashov, Holmusk-maintainer:          hi@holmusk.com-copyright:           2016-2018 Andrey Kartashov, 2018 Holmusk-category:            Library+maintainer:          Holmusk <tech@holmusk.com>+copyright:           2016-2018 Andrey Kartashov, 2018-2019 Holmusk+category:            Library, JSON, FCM build-type:          Simple extra-doc-files:     README.md-cabal-version:       2.0-tested-with:         GHC == 8.4.3+                     CHANGELOG.md+tested-with:         GHC == 8.2.2+                     GHC == 8.4.4+                     GHC == 8.6.5  source-repository head   type:     git   location: https://github.com/holmusk/fcm-client +common common-options+  build-depends:       base >= 4.10.1.0 && < 4.13++  default-language:    Haskell2010+  ghc-options:         -Wall+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -Wcompat+                       -Widentities+                       -Wredundant-constraints+                       -fhide-source-paths+ library+  import:             common-options   hs-source-dirs:     src   exposed-modules:    FCMClient-                    , FCMClient.JSON.Types-                    , FCMClient.Types+                        FCMClient.JSON.Types+                        FCMClient.Types -  build-depends:      base >= 4.9 && < 5.0-                    , aeson+  build-depends:      aeson                     , aeson-casing-                    , bytestring+                    , bytestring ^>= 0.10                     , containers                     , data-default-class                     , http-client@@ -35,19 +50,16 @@                     , http-types                     , lens                     , scientific-                    , text+                    , text ^>= 1.2                     , time -  default-language:   Haskell2010-  ghc-options:        -Wall -fwarn-tabs--executable            fcm-client+executable fcm-client+  import:             common-options   hs-source-dirs:     cli   main-is:            Main.hs   other-modules:      CliArgs -  build-depends:      base >= 4.9 && < 5.0-                    , aeson+  build-depends:      aeson                     , async                     , data-default-class                     , bytestring@@ -68,12 +80,12 @@   default-language:   Haskell2010  test-suite test+  import:             common-options   type:               exitcode-stdio-1.0   hs-source-dirs:     test   main-is:            Spec.hs -  build-depends:      base >= 4.9 && < 5.0-                    , aeson+  build-depends:      aeson                     , containers                     , data-default-class                     , fcm-client@@ -85,5 +97,4 @@                     , test-framework-quickcheck2                     , text -  default-language:   Haskell2010-  ghc-options:        -Wall -fwarn-tabs -fno-warn-orphans -fno-warn-missing-signatures+  ghc-options:        -fno-warn-orphans -fno-warn-missing-signatures
src/FCMClient.hs view
@@ -14,7 +14,7 @@ import qualified Data.Aeson as J import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L-import           Data.Monoid+import Data.Monoid ((<>)) import qualified Data.Text as T import qualified Data.Text.Encoding as T import           FCMClient.Types@@ -27,7 +27,7 @@ --   https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream fcmCallJSON :: (J.ToJSON req)             => B.ByteString -- ^ authorization key-            -> req -- ^ FCM JSON message, a typed model or a document object+            -> req -- ^ FCM JSON message, a typed model or a document object, see 'FCMClient.Types'             -> IO FCMResult fcmCallJSON authKey fcmMessage =   handle (\ (he :: HttpException) -> return $ FCMResultError . FCMClientHTTPError . T.pack . show $ he) $ do@@ -38,14 +38,14 @@                                               of Left e  -> FCMResultError $ FCMClientJSONError (T.pack e)                                                  Right b -> FCMResultSuccess b                         | rs == status400 = FCMResultError $ FCMErrorResponseInvalidJSON (textBody rb)-                        | rs == status401 = FCMResultError $ FCMErrorResponseInvalidAuth+                        | rs == status401 = FCMResultError FCMErrorResponseInvalidAuth                         | statusIsServerError rs = FCMResultError $ FCMServerError rs (textBody rb)-                        | otherwise              = FCMResultError $ FCMClientHTTPError $ "Unexpected response [" <> (T.pack . show $ rs) <> "]: " <> (textBody rb)+                        | otherwise              = FCMResultError $ FCMClientHTTPError $ "Unexpected response [" <> (T.pack . show) rs <> "]: " <> textBody rb -        textBody b = (T.decodeUtf8 . L.toStrict) b+        textBody = T.decodeUtf8 . L.toStrict  --- | Constructs an FCM JSON request, body and additional parameters such as+-- | Constructs an authenticated FCM JSON request, body and additional parameters such as --   proxy or http manager can be set for a customized HTTP call. fcmJSONRequest :: B.ByteString -- ^ authorization key                -> L.ByteString -- ^ JSON POST data
src/FCMClient/JSON/Types.hs view
@@ -16,6 +16,7 @@ , FCMNotification , fcmTitle , fcmBody+, fcmAndroidChannelId , fcmIcon , fcmSound , fcmTag@@ -28,7 +29,7 @@ , fcmTitleLocArgs , FCMMessage , fcmTo-, fcmRegistrationIDs+, fcmRegistrationIds , fcmCondition , fcmCollapseKey , fcmPriority@@ -110,6 +111,17 @@     -- Indicates notification body text.   , _fcmBody :: !(Maybe Text) +    -- | android_channel_id  Optional, string+    -- The notification's channel id (new in Android O). <https://developer.android.com/preview/features/notification-channels.html>+    --+    -- The app must create a channel with this channel ID before any notification+    -- with this channel ID is received.+    --+    -- If you don't send this channel ID in the request, or if the channel ID+    -- provided has not yet been created by the app, FCM uses the channel ID+    -- specified in the app manifest.+  , _fcmAndroidChannelId :: !(Maybe Text)+     -- | icon   Optional, string     -- Android: Indicates notification icon. Sets value to myicon for drawable resource myicon.   , _fcmIcon :: !(Maybe Text)@@ -186,7 +198,7 @@   instance Default FCMNotification where-  def = FCMNotification Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing+  def = FCMNotification Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing   -- | FCM Message as defined in https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream@@ -206,7 +218,7 @@     -- Use this parameter only for multicast messaging, not for single     -- recipients. Multicast messages (sending to more than 1 registration tokens) are     -- allowed using HTTP JSON format only.-  , _fcmRegistrationIDs :: !(Maybe (NonEmpty Text))+  , _fcmRegistrationIds :: !(Maybe (NonEmpty Text))      -- | condition  Optional, string     -- This parameter specifies a logical expression of conditions that determine@@ -424,12 +436,12 @@ instance Default FCMMessageResponse where   def = FCMMessageResponsePayload 0 0 0 0 Nothing -data FCMTopicResponseOk =+newtype FCMTopicResponseOk =   FCMTopicResponseOkPayload {     -- | Optional, number  The topic message ID when FCM has successfully     -- received the request and will attempt to deliver to all subscribed     -- devices.-    _fcmTopicMessageId :: !Integer+    _fcmTopicMessageId :: Integer   } deriving (Eq, Show)  $(makeLenses ''FCMTopicResponseOk )
src/FCMClient/Types.hs view
@@ -1,8 +1,4 @@-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE ScopedTypeVariables        #-}-{-# LANGUAGE TemplateHaskell            #-}   -- | Google Firebase Cloud Messaging model / JSON conversions.@@ -11,81 +7,30 @@ --   This module re-exports JSON types with a few convenience wrappers --   around selected fields. --+--   Models are constructed with lenses, starting with a default value, e.g:+--   >>> encode (def & fcmBody ?~ "fcm body")+--       "{\"body\":\"fcm body\"}"+-- module FCMClient.Types (-  J.FCMData-, FCMPriority(..)+  module Control.Lens+, module Data.Aeson+, module Data.Default.Class+, module Data.Scientific+, module FCMClient.JSON.Types , FCMLocValue(..)-, J.FCMNotification-, J.fcmTitle-, J.fcmBody-, J.fcmIcon-, J.fcmSound-, J.fcmTag-, J.fcmColor-, J.fcmBadge-, J.fcmClickAction-, J.fcmBodyLocKey+, FCMPriority(..) , fcmBodyLocArgs-, J.fcmTitleLocKey-, fcmTitleLocArgs-, J.FCMMessage-, J.fcmTo-, J.fcmRegistrationIDs-, J.fcmCondition-, J.fcmCollapseKey-, fcmPriority , fcmContentAvailable , fcmDelayWhileIdle-, J.fcmTimeToLive-, J.fcmRestrictedPackageName , fcmDryRun-, J.fcmData-, J.fcmNotification+, fcmPriority+, fcmTitleLocArgs , fcmWithNotification-, J.FCMResult ( J.FCMResultSuccess-              , J.FCMResultError-              )-, J._FCMResultSuccess-, J._FCMResultError-, J.FCMClientError ( J.FCMErrorResponseInvalidJSON-                   , J.FCMErrorResponseInvalidAuth-                   , J.FCMServerError-                   , J.FCMClientJSONError-                   , J.FCMClientHTTPError-                   )-, J.fcmErrorMessage-, J.fcmErrorHttpStatus-, J._FCMErrorResponseInvalidJSON-, J._FCMErrorResponseInvalidAuth-, J._FCMServerError-, J._FCMClientJSONError-, J._FCMClientHTTPError-, J.FCMResponseBody(..)-, J.FCMMessageResponse-, J._FCMMessageResponse-, J._FCMTopicResponse-, J.fcmCanonicalIds-, J.fcmFailure-, J.fcmMulticastId-, J.fcmResults-, J.fcmSuccess-, J.FCMMessageResponseResult(..)-, J._FCMMessageResponseResultOk-, J._FCMMessageResponseResultError-, J.FCMMessageResponseResultOk-, J.fcmMessageId-, J.fcmRegistrationId-, J.FCMTopicResponse(..)-, J.FCMTopicResponseOk-, J._FCMTopicResponseOk-, J._FCMTopicResponseError-, J.fcmTopicMessageId-, J.FCMError(..) ) where   import           Control.Lens-import           Data.Aeson+import           Data.Aeson (encode, decode) import           Data.Aeson.Types as J import           Data.Default.Class import           Data.List.NonEmpty (nonEmpty)@@ -96,11 +41,16 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.Encoding as TE+import           FCMClient.JSON.Types hiding ( fcmBodyLocArgs+                                             , fcmContentAvailable+                                             , fcmDelayWhileIdle+                                             , fcmDryRun+                                             , fcmPriority+                                             , fcmTitleLocArgs+                                             ) import qualified FCMClient.JSON.Types as J  -- data FCMPriority = FCMPriorityNormal                  | FCMPriorityHigh                  deriving (Eq, Show, Ord)@@ -137,8 +87,11 @@            => p [a] (f [a1])            -> p (Maybe Text) (f (Maybe Text)) aesonTxtPr =-  (prism' (fmap (LT.toStrict . TE.decodeUtf8)) (Just . (fmap (TE.encodeUtf8 . LT.fromStrict)))) .-    (prism' (fmap (encode) . nonEmpty)  (Just . fromMaybe [] . decode . fromMaybe ""))+  prism' (fmap (LT.toStrict . TE.decodeUtf8))+         (Just . fmap (TE.encodeUtf8 . LT.fromStrict))+  .+  prism' (fmap encode . nonEmpty)+         (Just . fromMaybe [] . decode . fromMaybe "")   -- | Typed lens focused on localized notification body arguments.@@ -161,7 +114,7 @@ fcmPriority :: (Applicative f)             => (FCMPriority -> f FCMPriority)             -> J.FCMMessage -> f J.FCMMessage-fcmPriority = J.fcmPriority . (prism' fcmPriorityToText (Just .textToFcmPriority))+fcmPriority = J.fcmPriority . prism' fcmPriorityToText (Just .textToFcmPriority)   where fcmPriorityToText FCMPriorityNormal = Nothing         fcmPriorityToText FCMPriorityHigh   = Just "high"         textToFcmPriority (Just "high")     = FCMPriorityHigh@@ -171,7 +124,7 @@  maybeBoolPr :: (Applicative f)             => (Bool -> f Bool)-            -> (Maybe Bool) -> f (Maybe Bool)+            -> Maybe Bool -> f (Maybe Bool) maybeBoolPr = prism' (\x -> if x then Just x else Nothing) (Just . fromMaybe False)  
test/Spec.hs view
@@ -1,6 +1,4 @@ {-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE RecordWildCards            #-}-{-# LANGUAGE ScopedTypeVariables #-}  module Main where@@ -26,73 +24,67 @@         ]  test_notificationJSON = do-  (encode (def :: FCMNotification)) @?= "{}"+  encode (def :: FCMNotification) @?= "{}" -  (encode $ fcmBody .~ (Just "fcm body") $ def) @?=+  encode (def & fcmBody ?~ "fcm body") @?=     "{\"body\":\"fcm body\"}" -  (encode $ fcmColor .~ (Just "#001122") $ def) @?=+  encode (def & fcmColor ?~ "#001122") @?=     "{\"color\":\"#001122\"}" -  (encode $ fcmTitleLocKey .~ (Just $ "loc title") $-              fcmTitleLocArgs .~ ([]) $-              def) @?=+  encode (def & fcmTitleLocKey ?~ "loc title"+              & fcmTitleLocArgs .~ [] ) @?=      "{\"title_loc_key\":\"loc title\"}" -  (encode $ fcmTitleLocKey .~ (Just $ "loc title") $-              fcmTitleLocArgs .~ ([FCMLocString "locStr", FCMLocNumber 1.0, FCMLocBool True]) $-              def) @?=-     "{\"title_loc_key\":\"loc title\",\"title_loc_args\":\"[\\\"locStr\\\",1,true]\"}"+  encode ( def & fcmTitleLocKey ?~ "loc title"+               & fcmTitleLocArgs .~ [FCMLocString "locStr", FCMLocNumber 1.0, FCMLocBool True]+         ) @?= "{\"title_loc_key\":\"loc title\",\"title_loc_args\":\"[\\\"locStr\\\",1,true]\"}"  -  (encode $ fcmBodyLocKey .~ (Just $ "loc body") $-              fcmBodyLocArgs .~ (["locStr", FCMLocNumber 1.0, FCMLocBool True]) $-              def) @?=-     "{\"body_loc_key\":\"loc body\",\"body_loc_args\":\"[\\\"locStr\\\",1,true]\"}"+  encode ( def & fcmBodyLocKey ?~ "loc body"+               & fcmBodyLocArgs .~ ["locStr", FCMLocNumber 1.0, FCMLocBool True]+         ) @?= "{\"body_loc_key\":\"loc body\",\"body_loc_args\":\"[\\\"locStr\\\",1,true]\"}"   test_messageJSON = do-  (encode (def :: FCMMessage)) @?= "{}"+  encode (def :: FCMMessage) @?= "{}" -  (encode $ fcmCollapseKey .~ (Just "ckey") $-            fcmTimeToLive .~ (Just 3) $-            fcmData .~ (Just (Map.fromList [("foo","bar")])) $-            def) @?=-    "{\"collapse_key\":\"ckey\",\"time_to_live\":3,\"data\":{\"foo\":\"bar\"}}"+  encode (def & fcmCollapseKey ?~ "ckey"+              & fcmTimeToLive ?~ 3+              & fcmData ?~ Map.fromList [("foo","bar")]+         ) @?= "{\"collapse_key\":\"ckey\",\"time_to_live\":3,\"data\":{\"foo\":\"bar\"}}" -  (encode $ fcmPriority .~ FCMPriorityHigh $ def) @?=+  encode (def & fcmPriority .~ FCMPriorityHigh) @?=     "{\"priority\":\"high\"}" -  (encode $ fcmPriority .~ FCMPriorityNormal $ def) @?= "{}"+  encode (def & fcmPriority .~ FCMPriorityNormal) @?= "{}" -  (encode $ fcmContentAvailable .~ False $ def) @?= "{}"+  encode (def & fcmContentAvailable .~ False) @?= "{}" -  (encode $ fcmContentAvailable .~ True $ def) @?= "{\"content_available\":true}"+  encode (def & fcmContentAvailable .~ True) @?=+    "{\"content_available\":true}" -  (decode "{\"content_available\":true}") ^.. (_Just . fcmContentAvailable) @?= [True]+  decode "{\"content_available\":true}" ^.. (_Just . fcmContentAvailable) @?= [True] -  (encode $ def &-           ( (fcmContentAvailable .~ True)-           . ( fcmWithNotification %~ ( (fcmBody .~ Just "n body")-                                      . (fcmTitle .~ Just "n title")-                                      )-             )-           )-    ) @?= "{\"content_available\":true,\"notification\":{\"title\":\"n title\",\"body\":\"n body\"}}"+  encode ( def & fcmContentAvailable .~ True+               & fcmWithNotification %~ ( (fcmBody ?~ "n body")+                                        . (fcmTitle ?~ "n title")+                                        )+         ) @?= "{\"content_available\":true,\"notification\":{\"title\":\"n title\",\"body\":\"n body\"}}"   test_responseJSON = do-  (encode (FCMMessageResponse def)) @?= "{\"multicast_id\":0,\"success\":0,\"failure\":0,\"canonical_ids\":0}"+  encode (FCMMessageResponse def) @?= "{\"multicast_id\":0,\"success\":0,\"failure\":0,\"canonical_ids\":0}" -  (encode (FCMTopicResponseOk def)) @?= "{\"message_id\":0}"+  encode (FCMTopicResponseOk def) @?= "{\"message_id\":0}" -  (decode "{\"message_id\":6538060933731373360}") ^..+  decode "{\"message_id\":6538060933731373360}" ^..     (_Just . _FCMTopicResponse . _FCMTopicResponseOk . fcmTopicMessageId ) @?= [6538060933731373360]    let jRes = "{\"multicast_id\":4984205480219716339,\"success\":0,\"failure\":1,\"canonical_ids\":0,\"results\":[{\"error\":\"NotRegistered\"}]}" -  (decode jRes) ^..+  decode jRes ^..     (_Just . _FCMMessageResponse . fcmResults . _Just . traverse .  _FCMMessageResponseResultError ) @?=       [FCMErrorNotRegistered] -  (decode jRes) ^.. (_Just . _FCMMessageResponse . fcmMulticastId ) @?= [4984205480219716339]+  decode jRes ^.. (_Just . _FCMMessageResponse . fcmMulticastId ) @?= [4984205480219716339]