-- | -- This module provides functions wrapping the Azure REST API web methods. module Network.TableStorage.API ( queryTables, createTable, createTableIfNecessary, deleteTable, insertEntity, updateEntity, mergeEntity, deleteEntity, queryEntity, queryEntities, defaultEntityQuery, defaultAccount ) where import Network.HTTP import Text.XML.Light import Text.Printf import Network.TableStorage.Types import Network.TableStorage.Auth import Network.TableStorage.Request import Network.TableStorage.Response import Network.TableStorage.Atom import Data.Time.Clock (getCurrentTime) import Data.Maybe (fromMaybe) -- | -- Parse the response body of the Query Tables web method -- parseQueryTablesResponse :: Response_String -> Either String [String] parseQueryTablesResponse = parseXmlResponseOrError (2, 0, 0) readTables where readTables :: Element -> Maybe [String] readTables feed = sequence $ do entry <- findChildren (qualifyAtom "entry") feed return $ readTableName entry readTableName entry = do content <- findChild (qualifyAtom "content") entry properties <- findChild (qualifyMetadata "properties") content tableName <- findChild (qualifyDataServices "TableName") properties return $ strContent tableName -- | -- List the names of tables for an account or returns an error message -- queryTables :: Account -> IO (Either String [String]) queryTables acc = do let resource = "/Tables" response <- authenticatedRequest acc GET [] resource resource "" return $ response >>= parseQueryTablesResponse -- | -- Construct the request body for the Create Table web method -- createTableXml :: String -> IO Element createTableXml tableName = wrapContent $ propertyList [("TableName", EdmString $ Just tableName)] -- | -- Creates a new table with the specified name or returns an error message -- createTable :: Account -> String -> IO (Either String ()) createTable acc tableName = do let resource = "/Tables" requestXml <- createTableXml tableName response <- authenticatedRequest acc POST [] resource resource $ showTopElement requestXml return $ response >>= parseEmptyResponse (2, 0, 1) -- | -- Creates a new table with the specified name if it does not already exist, or returns an erro message -- createTableIfNecessary :: Account -> String -> IO (Either String ()) createTableIfNecessary acc tableName = do tables <- queryTables acc case tables of Left err -> return $ Left err Right tables' -> if any (== tableName) tables' then return $ Right () else createTable acc tableName -- | -- Deletes the table with the specified name or returns an error message -- deleteTable :: Account -> String -> IO (Either String ()) deleteTable acc tableName = do let resource = printf "/Tables('%s')" tableName response <- authenticatedRequest acc DELETE [] resource resource "" return $ response >>= parseEmptyResponse (2, 0, 4) -- | -- Construct the request body for the Insert Entity web method -- createInsertEntityXml :: Entity -> IO Element createInsertEntityXml entity = do time <- getCurrentTime wrapContent $ propertyList $ [ ("PartitionKey", EdmString $ Just $ ekPartitionKey $ entityKey entity), ("RowKey", EdmString $ Just $ ekRowKey $ entityKey entity), ("Timestamp", EdmDateTime $ Just time) ] ++ entityColumns entity -- | -- Inserts an entity into the table with the specified name or returns an error message -- insertEntity :: Account -> String -> Entity -> IO (Either String ()) insertEntity acc tableName entity = do let resource = printf "/%s" tableName requestXml <- createInsertEntityXml entity response <- authenticatedRequest acc POST [] resource resource $ showTopElement requestXml return $ response >>= parseEmptyResponse (2, 0, 1) -- | -- Shared method to update or merge an existing entity. The only difference between the -- two methods is the request method used. -- updateOrMergeEntity :: RequestMethod -> Account -> String -> Entity -> IO (Either String ()) updateOrMergeEntity method acc tableName entity = do let resource = entityKeyResource tableName $ entityKey entity let additionalHeaders = [ Header (HdrCustom "If-Match") "*" ] requestXml <- createInsertEntityXml entity response <- authenticatedRequest acc method additionalHeaders resource resource $ showTopElement requestXml return $ response >>= parseEmptyResponse (2, 0, 4) -- | -- Updates the specified entity (possibly removing columns) or returns an error message -- updateEntity :: Account -> String -> Entity -> IO (Either String ()) updateEntity = updateOrMergeEntity PUT -- | -- Merges the specified entity (without removing columns) or returns an error message -- mergeEntity :: Account -> String -> Entity -> IO (Either String ()) mergeEntity = updateOrMergeEntity (Custom "MERGE") -- | -- Deletes the entity with the specified key or returns an error message -- deleteEntity :: Account -> String -> EntityKey -> IO (Either String ()) deleteEntity acc tableName key = do let resource = entityKeyResource tableName key let additionalHeaders = [ Header (HdrCustom "If-Match") "*" ] response <- authenticatedRequest acc DELETE additionalHeaders resource resource "" return $ response >>= parseEmptyResponse (2, 0, 4) -- | -- Parse an Atom entry as an entity -- readEntity :: Element -> Maybe Entity readEntity entry = do content <- findChild (qualifyAtom "content") entry properties <- findChild (qualifyMetadata "properties") content partitionKey <- findChild (qualifyDataServices "PartitionKey") properties rowKey <- findChild (qualifyDataServices "RowKey") properties let columnData = filterChildren filterProperties properties columns <- mapM elementToColumn columnData return Entity { entityKey = EntityKey { ekPartitionKey = strContent partitionKey, ekRowKey = strContent rowKey }, entityColumns = columns } where filterProperties el | elName el == qualifyDataServices "PartitionKey" = False | elName el == qualifyDataServices "RowKey" = False | otherwise = qURI (elName el) == Just dataServicesNamespace elementToColumn el = let propertyName = qName $ elName el in let typeAttr = fromMaybe "Edm.String" $ findAttr (qualifyMetadata "type") el in let typeNull = maybe False ("true" ==) $ findAttr (qualifyMetadata "null") el in (\val -> (propertyName, val)) `fmap` parseEntityColumn typeNull typeAttr (strContent el) -- | -- Parse the response body of the Query Entity web method -- parseQueryEntityResponse :: Response_String -> Either String Entity parseQueryEntityResponse = parseXmlResponseOrError (2, 0, 0) readEntity where -- | -- Returns the entity with the specified table name and key or an error message -- queryEntity :: Account -> String -> EntityKey -> IO (Either String Entity) queryEntity acc tableName key = do let resource = entityKeyResource tableName key response <- authenticatedRequest acc GET [] resource resource "" return $ response >>= parseQueryEntityResponse -- | -- Parse the response body of the Query Entities web method -- parseQueryEntitiesResponse :: Response_String -> Either String [Entity] parseQueryEntitiesResponse = parseXmlResponseOrError (2, 0, 0) readEntities where readEntities :: Element -> Maybe [Entity] readEntities feed = sequence $ do entry <- findChildren (qualifyAtom "entry") feed return $ readEntity entry -- | -- Returns a collection of entities by executing the specified query or returns an error message -- queryEntities :: Account -> String -> EntityQuery -> IO (Either String [Entity]) queryEntities acc tableName query = do let canonicalizedResource = printf "/%s()" tableName let queryString = buildQueryString query let resource = printf "%s?%s" canonicalizedResource queryString response <- authenticatedRequest acc GET [] resource canonicalizedResource "" return $ response >>= parseQueryEntitiesResponse -- | -- An empty query with no filters and no specified page size -- defaultEntityQuery :: EntityQuery defaultEntityQuery = EntityQuery { eqPageSize = Nothing, eqFilter = Nothing } -- | -- Constructs an Account with the default values for Port and Resource Prefix defaultAccount :: AccountKey -> String -> String -> Account defaultAccount key name hostname = Account { accountScheme = "http", accountHost = hostname, accountPort = 80, accountKey = key, accountName = name, accountResourcePrefix = "" }