packages feed

simplenote-1.0: Network/Simplenote.hs

-- |
-- Module    : Network.Simplenote
-- Copyright : (c) 2009 marius a. eriksen
-- License   : BSD3
-- 
-- Maintainer:  marius a. eriksen <marius@monkey.org>
-- Stability :  provisional
-- 
-- Haskell API for Simplenote (http://www.simplenoteapp.com/)
-- 

module Network.Simplenote 
  (
  -- * Types for representing structured simplenote responses
    Index(..)
  , IndexEntry(..)
  , SearchResults(..)
  , SearchResult(..)

  -- * API methods
  , authenticate
  , index
  , note
  , search
  , delete
  , update
  ) where

import Control.Monad     (unless)
import Control.Exception (throw)
import Data.Maybe        (isJust, catMaybes)
import Text.JSON         ( JSON(..), JSValue(..), Result(..)
                         , fromJSObject, fromJSString, decode)
import Text.Printf       (printf)

import Network.Simplenote.Request

-- | Index responses
data Index      = Index [IndexEntry]
                  deriving (Show)
data IndexEntry = IndexEntry 
                    String      -- ^ key
                    String      -- ^ modify date
                    Bool        -- ^ deleted
                  deriving (Show)

-- | Search response
data SearchResults = SearchResults Int [SearchResult]
                     deriving (Show)
data SearchResult  = SearchResult 
                       String   -- ^ key
                       String   -- ^ content
                     deriving (Show)

-- Implement JSON parsers for the above for decoding responses:
instance JSON Index where
  showJSON _ = error "showJSON not implemented"
  readJSON (JSArray v) = mapM readJSON v >>= return . Index

instance JSON IndexEntry where
  showJSON _ = error "showJSON not implemented"
  readJSON (JSObject v) = do
    let kvs = fromJSObject v
        vs  = map (flip lookup kvs) ["key", "modify", "deleted"]
    unless (all isJust vs) $ fail "Missing keys."
    let vs' = catMaybes vs
    key     <- readJSON $ vs' !! 0
    modify  <- readJSON $ vs' !! 1
    deleted <- readJSON $ vs' !! 2
    return $ IndexEntry key modify deleted

instance JSON SearchResults where
  showJSON _ = error "showJSON not implemented"
  readJSON (JSObject v) = do
    -- Extract values in the Maybe monad:
    let values = do JSObject response' <- lookup "Response" $ fromJSObject v
                    let response = fromJSObject response'
                    totalRecords    <- lookup "totalRecords" response
                    JSArray results <- lookup "Results" response
                    return (totalRecords, results)

    case values of
      Just (totalRecords, results) -> do
        totalRecords' <- readJSON totalRecords
        results'      <- mapM readJSON results
        return $ SearchResults totalRecords' results'
      Nothing -> fail "JSON parse error"

instance JSON SearchResult where
  showJSON _ = error "showJSON not implemented"
  readJSON (JSObject v) = do
    let object = fromJSObject v
        values = do key     <- lookup "key" object
                    content <- lookup "content" object
                    return (key, content)
    case values of
      Just (key, content) -> do
        key'     <- readJSON key
        content' <- readJSON content
        return $ SearchResult key' content'
      Nothing -> fail "JSON parse error"

-- | Retrieve a note with the given key.
note :: Token                   -- ^ authentication token
     -> String                  -- ^ note key
     -> IO (Either String String)
note token key = get token "note" [("key", key)]

-- | List notes
index :: Token -> IO (Either String Index)
index token = decoded $ get token "index" []

-- | Search for text within all notes
search :: Token                 -- ^ authentication token
       -> String                -- ^ search string (query)
       -> Int                   -- ^ maximum number of results to return
       -> Int                   -- ^ offset into results
       -> IO (Either String SearchResults)
search token query maxResults offset =
  decoded $ get token "search" [ ("query", query)
                               , ("results", show maxResults)
                               , ("offset", show offset)
                               ]

-- | Delete the given note
delete :: Token                 -- ^ authentication token
       -> String                -- ^ note key
       -> IO (Either String ())
delete token key = do
  result <- get token "delete" [("key", key)]
  case result of 
    Left what -> return $ Left what
    Right _   -> return $ Right ()

-- | Update (or create) a note with new content. If 'Nothing' is
-- passed as a note ID, a new note is created, otherwise the named
-- note is updated with the new body. The note key is always returned.
-- 
-- Examples:
-- 
-- > Right token <- authenticate "foo@bob.com" "password"
-- > update token Nothing "a test note"
-- > search token "note" 1 0
update :: Token                 -- ^ authentication token
       -> Maybe String          -- ^ note key or 'Nothing'
       -> String                -- ^ new note body
       -> IO (Either String String)
update token key body = 
  let args = case key of 
               Just key' -> [("key", key')]
               Nothing   -> []
  in
    post token "note" args body

-- Internal:

decoded response = 
  response >>= return . either Left tryDecode
  where
    tryDecode body = 
      case decode body of
        Ok decoded -> Right decoded
        Error what -> Left $ printf "JSON decode error: %s" what