packages feed

haste-gapi-0.1.0.0: Haste/GAPI.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : Haste.GAPI
Description : Entry Module for haste-gapi
Copyright   : (c) Jonathan Skårstedt, 2016
License     : MIT
Maintainer  : jonathan.skarstedt@gmail.com
Stability   : experimental
Portability : Haste

Example of a login to the Google API. Note that @config@ is defined
elsewhere.

@

main :: IO ()
main = withGAPI config $ \token -> case token of
  OA2Success {} ->
    putStrLn $ show token

  OA2Error {errorMsg = e} ->
    putStrLn $ "Your success token is invalid (" ++ e ++ ")"

@

The login starts with the @withGAPI@, which will load the Google API
environment. If the script is compiled with @--onexec@ flag, the load will be
significally faster as we don't have to wait until the script has fully loaded
until we download the Google API hooks.

When loading is done, a @OAuth2Token@ is generated, and we may inspect it
to see wether the login is successful or not. On success, we continue with
showing the token. If not, we print an error instead. Note that the
@withGAPI@ function ought to only be invoked once.


Example of a request:

@

greet :: IO ()
greet = runR $ do
  response <- request "plus\/v1\/people\/me" def
  Just [name, pic] \<- sequence <$\> mapM (lookupVal response) [
   "result.displayName",
   "result.image.url"]
  liftIO . putStrLn $ "Hello " ++  name ++ "! \<br /\>"
    ++ "You look like this: \<br \/\>\<img src='"++ pic ++ "' \/\>"

@

The reqeuest above will greet the user by name, fetched from the Google+ API.
A response is generated by taking a Path and some Params to the @request@
function, which will execute it and return a response.

After that we look up some fields in the result, namely /result.displayName/
and /result.image.url/. The first will be bound to name, and the second to
pic. These two elements are both strings, by Haste.Foreign magic.

After that we use @RequestM@s @liftIO@ (as it's an instance of the MonadIO
class) and present a pretty HTML string!
-}
module Haste.GAPI (
  -- | = Connecting to the Google API
  withGAPI,
  oa2success,
  getToken,
  Config(..),
  OAuth2Token(..),
  -- | = Creating Requests
  module Haste.GAPI.Request,
  -- | = Handling Results 
  module Haste.GAPI.Result,
  -- | = Common types when working with Google API libraries
  module Haste.GAPI.Types

  ) where 

import Haste.Foreign hiding (get, has, hasAll)
import qualified Haste.Foreign as FFI

-- GHC 7.8 compatibility
import Data.Functor ((<$>))

import Haste.GAPI.Request 
import Haste.GAPI.Result
import Haste.GAPI.Types
import Data.Default
-- import Control.Monad
import Control.Applicative

-- Datatypes -----------------------------------------------------------------

-- | Google API configuration. For an in-detail description of what each field
--    does, please see the <https://developers.google.com/api-client-library/javascript/reference/referencedocs Google API Reference>,
--    especially the methods @gapi.auth.authorize@ and @gapi.client.setApiKey@.
data Config = Config {
  -- | Client ID to generate an authentification token from. 
  clientID  :: String,
  -- | The API key for your application
  apiKey    :: String,
  -- | Here you enter the availiable scopes for your application.
  scopes    :: String,
  -- | If true, the token an attempt will be made to refresh it behind the
  --    cenes
  immediate :: Bool    
  }
              
instance Show Config where
  show (Config cid key scopes' imm)
    = "\nConfig: " ++ concatMap (++ "\n\t") [cid, key, scopes', show imm]

instance ToAny Config where
  toAny cfg = toObject [("clientID",  toAny $ clientID cfg),
                        ("apiKey",    toAny $ apiKey cfg),
                        ("scopes",    toAny $ scopes cfg),
                        ("immediate", toAny $ immediate cfg)]

-- | OAuth2Token, the authentication tokens used by the Google API.
data OAuth2Token = OA2Success {
  -- | Authenticated access token 
  accessToken :: String,
  -- | Expiration of the token 
  expiresIn   :: String,
  -- | Google API Scopes related to this token.
  state       :: String
  }
                 | OA2Error {
                     errorMsg :: String,
                     state    :: String
                     }

instance Show OAuth2Token where
  show t = if oa2success t
           then "Success Token: '" ++ shorten (accessToken t)
                ++ "'\n\t for scopes: '" ++ state t
                ++ "'\n\t expires in: "++ expiresIn t ++ "s"
           else "Failure Token: " ++ errorMsg t
    where shorten :: String -> String
          shorten str | length str < 16 = str
                      | otherwise       = take 32 str ++ "..."


instance FromAny OAuth2Token where
  fromAny oa2 = do
    success <- FFI.has oa2 "access_token"
    if success
      then OA2Success <$> FFI.get oa2 "access_token"
           <*> FFI.get oa2 "expires_in"
           <*> FFI.get oa2 "state"
      else OA2Error <$> FFI.get oa2 "error"
           <*> FFI.get oa2 "state"
 
-- Exported functions --------------------------------------------------------
-- | Returns true if the token represents a successful authentication
oa2success :: OAuth2Token -> Bool
oa2success OA2Success {} = True
oa2success _ = False

-- | Loads the Google API, inserts Google API headers and then executes
--    an action. 
withGAPI :: Config -> (OAuth2Token -> IO ()) -> IO ()
withGAPI cfg handler = do
  loadGAPI cfg handler
  loadGAPIExternals "GAPILoader"

-- | Exports and coordinate loading of the Google API.
loadGAPI :: Config -> (OAuth2Token -> IO ()) -> IO ()
loadGAPI = loadGAPI' "GAPILoader"

-- | Loads the Google API with a custom loader name
loadGAPI' :: String -> Config -> (OAuth2Token -> IO ()) -> IO ()
loadGAPI' symbol cfg handler
  = exportLoaderSymbol symbol $ loadClient cfg $ auth cfg handler

-- | Returns the token from the current Google API state
getToken :: IO OAuth2Token
getToken = ffi "(function() {return gapi.auth.getToken();})"

-- | Loads the GAPI Client 
loadClient :: Config -> IO () -> IO ()
loadClient = ffi "(function(cfg, auth){\
\gapi.client.setApiKey(cfg.apiKey); \
\window.setTimeout(auth, 1);})"

-- | Authenticates the user. Should be invoked by loadClient
auth :: Config -> (OAuth2Token -> IO ()) -> IO ()
auth = ffi "(function(cfg, ah)\
\{gapi.auth.authorize({\
 \'client_id': cfg.clientID, \
 \'scope': cfg.scopes, \
 \'immediate': cfg.immediate}, \
\ah);})"

-- | Export the loader symbol 
exportLoaderSymbol :: String -> IO () -> IO ()
exportLoaderSymbol = ffi "(function(s, f) {window[s] = f;})"

-- | Loads the external GAPI scripts
loadGAPIExternals :: String -> IO ()
loadGAPIExternals = ffi "(function(sym) {\
\var s = document.createElement('script');\
\s.setAttribute('src', 'https://apis.google.com/js/client.js?onload=' + sym);\
\s.setAttribute('type', 'text/javascript');\
\document.head.appendChild(s);})"