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 +25/−0
- README.md +2/−1
- cli/CliArgs.hs +32/−25
- cli/Main.hs +18/−18
- fcm-client.cabal +35/−24
- src/FCMClient.hs +6/−6
- src/FCMClient/JSON/Types.hs +17/−5
- src/FCMClient/Types.hs +27/−74
- test/Spec.hs +32/−40
+ 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]