aws 0.9.5 → 0.10
raw patch · 24 files changed
+3519/−531 lines, 24 filesdep +QuickCheckdep +attoparsecdep +errorsdep ~aesondep ~basedep ~bytestringnew-component:exe:DynamoDb
Dependencies added: QuickCheck, attoparsec, errors, exceptions, quickcheck-instances, safe, scientific, tagged, tasty, tasty-quickcheck
Dependency ranges changed: aeson, base, bytestring, conduit, data-default, http-conduit, http-types, mtl, text, transformers
Files
- Aws/Core.hs +41/−23
- Aws/DynamoDb.hs +20/−0
- Aws/DynamoDb/Commands.hs +16/−4
- Aws/DynamoDb/Commands/GetItem.hs +92/−0
- Aws/DynamoDb/Commands/PutItem.hs +114/−0
- Aws/DynamoDb/Commands/Query.hs +160/−0
- Aws/DynamoDb/Commands/Scan.hs +125/−0
- Aws/DynamoDb/Commands/Table.hs +181/−151
- Aws/DynamoDb/Commands/UpdateItem.hs +160/−0
- Aws/DynamoDb/Core.hs +1136/−126
- Aws/S3/Commands.hs +2/−0
- Aws/S3/Commands/CopyObject.hs +6/−4
- Aws/S3/Commands/GetBucketLocation.hs +56/−0
- Aws/S3/Commands/HeadObject.hs +10/−3
- Aws/S3/Commands/PutObject.hs +4/−2
- Aws/S3/Core.hs +6/−1
- Aws/Sqs/Commands/Message.hs +615/−112
- Aws/Sqs/Core.hs +12/−5
- Examples/DynamoDb.hs +108/−0
- Examples/Sqs.hs +47/−21
- README.org +13/−6
- aws.cabal +134/−73
- tests/Sqs/Main.hs +316/−0
- tests/Utils.hs +145/−0
Aws/Core.hs view
@@ -134,13 +134,16 @@ import qualified Text.XML as XML import qualified Text.XML.Cursor as Cu import Text.XML.Cursor hiding (force, forceM)+------------------------------------------------------------------------------- + -- | Types that can be logged (textually). class Loggable a where toLogText :: a -> T.Text --- | A response with metadata. Can also contain an error response, or an internal error, via 'Attempt'.---+-- | A response with metadata. Can also contain an error response, or+-- an internal error, via 'Attempt'.+-- -- Response forms a Writer-like monad. data Response m a = Response { responseMetadata :: m , responseResult :: Either E.SomeException a }@@ -191,10 +194,12 @@ -- -- Note that for debugging, there is an instance for 'L.ByteString'. class Monoid (ResponseMetadata resp) => ResponseConsumer req resp where- -- | Metadata associated with a response. Typically there is one metadata type for each AWS service.+ -- | Metadata associated with a response. Typically there is one+ -- metadata type for each AWS service. type ResponseMetadata resp - -- | Response parser. Takes the corresponding request, an 'IORef' for metadata, and HTTP response data.+ -- | Response parser. Takes the corresponding request, an 'IORef'+ -- for metadata, and HTTP response data. responseConsumer :: req -> IORef (ResponseMetadata resp) -> HTTPResponseConsumer resp -- | Does not parse response. For debugging.@@ -215,12 +220,14 @@ class ListResponse resp item | resp -> item where listResponse :: resp -> [item] + -- | Associates a request type and a response type in a bi-directional way.------ This allows the type-checker to infer the response type when given the request type and vice versa.------ Note that the actual request generation and response parsing resides in 'SignQuery' and 'ResponseConsumer'--- respectively.+-- +-- This allows the type-checker to infer the response type when given+-- the request type and vice versa.+-- +-- Note that the actual request generation and response parsing+-- resides in 'SignQuery' and 'ResponseConsumer' respectively. class (SignQuery r, ResponseConsumer r a, Loggable (ResponseMetadata a)) => Transaction r a | r -> a, a -> r@@ -370,7 +377,7 @@ data Protocol = HTTP | HTTPS- deriving (Show)+ deriving (Eq,Read,Show,Ord,Typeable) -- | The default port to be used for a protocol if no specific port is specified. defaultPort :: Protocol -> Int@@ -458,18 +465,28 @@ , fmap (\auth -> ("Authorization", auth)) mauth] ++ sqAmzHeaders ++ sqOtherHeaders- , HTTP.requestBody = case sqMethod of- PostQuery -> HTTP.RequestBodyLBS . Blaze.toLazyByteString $ HTTP.renderQueryBuilder False sqQuery- _ -> case sqBody of- Nothing -> HTTP.RequestBodyBuilder 0 mempty- Just x -> x+ , HTTP.requestBody = ++ -- An explicityly defined body parameter should overwrite everything else.+ case sqBody of+ Just x -> x+ Nothing -> + -- a POST query should convert its query string into the body+ case sqMethod of+ PostQuery -> HTTP.RequestBodyLBS . Blaze.toLazyByteString $ + HTTP.renderQueryBuilder False sqQuery+ _ -> HTTP.RequestBodyBuilder 0 mempty+ , HTTP.decompress = HTTP.alwaysDecompress , HTTP.checkStatus = \_ _ _ -> Nothing }- where contentType = case sqMethod of- PostQuery -> Just "application/x-www-form-urlencoded; charset=utf-8"- _ -> sqContentType- checkDate f mb = maybe (f <$> mb) (const Nothing) $ lookup "date" sqOtherHeaders+ where + checkDate f mb = maybe (f <$> mb) (const Nothing) $ lookup "date" sqOtherHeaders+ -- An explicitly defined content-type should override everything else.+ contentType = sqContentType `mplus` defContentType+ defContentType = case sqMethod of+ PostQuery -> Just "application/x-www-form-urlencoded; charset=utf-8"+ _ -> Nothing -- | Create a URI fro a 'SignedQuery' object. --@@ -651,9 +668,10 @@ debugServiceConfig :: config debugServiceConfig = defServiceConfig --- | @queryList f prefix xs@ constructs a query list from a list of elements @xs@, using a common prefix @prefix@,--- and a transformer function @f@.---+-- | @queryList f prefix xs@ constructs a query list from a list of+-- elements @xs@, using a common prefix @prefix@, and a transformer+-- function @f@.+-- -- A dot (@.@) is interspersed between prefix and generated key. -- -- Example:@@ -751,7 +769,7 @@ instance E.Exception FormException -- | No credentials were found and an invariant was violated.-newtype NoCredentialsException = NoCredentialsException { noCredentialsErrorMesage :: String }+newtype NoCredentialsException = NoCredentialsException { noCredentialsErrorMessage :: String } deriving (Show, Typeable) instance E.Exception NoCredentialsException
+ Aws/DynamoDb.hs view
@@ -0,0 +1,20 @@+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynaboDb+-- Copyright : Ozgun Ataman, Soostone Inc.+-- License : BSD3+--+-- Maintainer : Ozgun Ataman <oz@soostone.com>+-- Stability : experimental+--+----------------------------------------------------------------------------++module Aws.DynamoDb+ ( module Aws.DynamoDb.Core+ , module Aws.DynamoDb.Commands+ ) where++-------------------------------------------------------------------------------+import Aws.DynamoDb.Commands+import Aws.DynamoDb.Core+-------------------------------------------------------------------------------
Aws/DynamoDb/Commands.hs view
@@ -1,5 +1,17 @@-module Aws.DynamoDb.Commands(- module Aws.DynamoDb.Commands.Table-) where+module Aws.DynamoDb.Commands+ ( module Aws.DynamoDb.Commands.GetItem+ , module Aws.DynamoDb.Commands.PutItem+ , module Aws.DynamoDb.Commands.Query+ , module Aws.DynamoDb.Commands.Scan+ , module Aws.DynamoDb.Commands.Table+ , module Aws.DynamoDb.Commands.UpdateItem+ ) where -import Aws.DynamoDb.Commands.Table+-------------------------------------------------------------------------------+import Aws.DynamoDb.Commands.GetItem+import Aws.DynamoDb.Commands.PutItem+import Aws.DynamoDb.Commands.Query+import Aws.DynamoDb.Commands.Scan+import Aws.DynamoDb.Commands.Table+import Aws.DynamoDb.Commands.UpdateItem+-------------------------------------------------------------------------------
+ Aws/DynamoDb/Commands/GetItem.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynamoDb.Commands.GetItem+-- Copyright : Soostone Inc+-- License : BSD3+--+-- Maintainer : Ozgun Ataman <ozgun.ataman@soostone.com>+-- Stability : experimental+--+--+----------------------------------------------------------------------------++module Aws.DynamoDb.Commands.GetItem where++-------------------------------------------------------------------------------+import Control.Applicative+import Data.Aeson+import Data.Default+import qualified Data.Text as T+-------------------------------------------------------------------------------+import Aws.Core+import Aws.DynamoDb.Core+-------------------------------------------------------------------------------+++-- | A GetItem query that fetches a specific object from DDB.+--+-- See: @http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/API_GetItem.html@+data GetItem = GetItem {+ giTableName :: T.Text+ , giKey :: PrimaryKey+ , giAttrs :: Maybe [T.Text]+ -- ^ Attributes to get. 'Nothing' grabs everything.+ , giConsistent :: Bool+ -- ^ Whether to issue a consistent read.+ , giRetCons :: ReturnConsumption+ -- ^ Whether to return consumption stats.+ } deriving (Eq,Show,Read,Ord)+++-------------------------------------------------------------------------------+-- | Construct a minimal 'GetItem' request.+getItem+ :: T.Text -- ^ Table name+ -> PrimaryKey -- ^ Primary key+ -> GetItem+getItem tn k = GetItem tn k Nothing False def+++-- | Response to a 'GetItem' query.+data GetItemResponse = GetItemResponse {+ girItem :: Maybe Item+ , girConsumed :: Maybe ConsumedCapacity+ } deriving (Eq,Show,Read,Ord)+++instance Transaction GetItem GetItemResponse+++instance ToJSON GetItem where+ toJSON GetItem{..} = object $+ maybe [] (return . ("AttributesToGet" .=)) giAttrs +++ [ "TableName" .= giTableName+ , "Key" .= giKey+ , "ConsistentRead" .= giConsistent+ , "ReturnConsumedCapacity" .= giRetCons+ ]+++instance SignQuery GetItem where+ type ServiceConfiguration GetItem = DdbConfiguration+ signQuery gi = ddbSignQuery "GetItem" gi++++instance FromJSON GetItemResponse where+ parseJSON (Object v) = GetItemResponse+ <$> v .:? "Item"+ <*> v .:? "ConsumedCapacity"+ parseJSON _ = fail "GetItemResponse must be an object."+++instance ResponseConsumer r GetItemResponse where+ type ResponseMetadata GetItemResponse = DdbResponse+ responseConsumer _ ref resp = ddbResponseConsumer ref resp+++instance AsMemoryResponse GetItemResponse where+ type MemoryResponse GetItemResponse = GetItemResponse+ loadToMemory = return
+ Aws/DynamoDb/Commands/PutItem.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynamoDb.Commands.GetItem+-- Copyright : Soostone Inc+-- License : BSD3+--+-- Maintainer : Ozgun Ataman <ozgun.ataman@soostone.com>+-- Stability : experimental+--+-- @http:\/\/docs.aws.amazon.com\/amazondynamodb\/latest\/APIReference\/API_PutItem.html@+----------------------------------------------------------------------------++module Aws.DynamoDb.Commands.PutItem where++-------------------------------------------------------------------------------+import Control.Applicative+import Data.Aeson+import Data.Default+import qualified Data.Text as T+-------------------------------------------------------------------------------+import Aws.Core+import Aws.DynamoDb.Core+-------------------------------------------------------------------------------+++data PutItem = PutItem {+ piTable :: T.Text+ -- ^ Target table+ , piItem :: Item+ -- ^ An item to Put. Attributes here will replace what maybe under+ -- the key on DDB.+ , piExpect :: Conditions+ -- ^ (Possible) set of expections for a conditional Put+ , piReturn :: UpdateReturn+ -- ^ What to return from this query.+ , piRetCons :: ReturnConsumption+ , piRetMet :: ReturnItemCollectionMetrics+ } deriving (Eq,Show,Read,Ord)+++-------------------------------------------------------------------------------+-- | Construct a minimal 'PutItem' request.+putItem :: T.Text+ -- ^ A Dynamo table name+ -> Item+ -- ^ Item to be saved+ -> PutItem+putItem tn it = PutItem tn it def def def def+++instance ToJSON PutItem where+ toJSON PutItem{..} =+ object $ expectsJson piExpect +++ [ "TableName" .= piTable+ , "Item" .= piItem+ , "ReturnValues" .= piReturn+ , "ReturnConsumedCapacity" .= piRetCons+ , "ReturnItemCollectionMetrics" .= piRetMet+ ]++++data PutItemResponse = PutItemResponse {+ pirAttrs :: Maybe Item+ -- ^ Old attributes, if requested+ , pirConsumed :: Maybe ConsumedCapacity+ -- ^ Amount of capacity consumed+ , pirColMet :: Maybe ItemCollectionMetrics+ -- ^ Collection metrics if they have been requested.+ } deriving (Eq,Show,Read,Ord)++++instance Transaction PutItem PutItemResponse+++instance SignQuery PutItem where+ type ServiceConfiguration PutItem = DdbConfiguration+ signQuery gi = ddbSignQuery "PutItem" gi+++instance FromJSON PutItemResponse where+ parseJSON (Object v) = PutItemResponse+ <$> v .:? "Attributes"+ <*> v .:? "ConsumedCapacity"+ <*> v .:? "ItemCollectionMetrics"+ parseJSON _ = fail "PutItemResponse must be an object."+++instance ResponseConsumer r PutItemResponse where+ type ResponseMetadata PutItemResponse = DdbResponse+ responseConsumer _ ref resp = ddbResponseConsumer ref resp+++instance AsMemoryResponse PutItemResponse where+ type MemoryResponse PutItemResponse = PutItemResponse+ loadToMemory = return++++++++
+ Aws/DynamoDb/Commands/Query.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynamoDb.Commands.Query+-- Copyright : Soostone Inc+-- License : BSD3+--+-- Maintainer : Ozgun Ataman <ozgun.ataman@soostone.com>+-- Stability : experimental+--+-- Implementation of Amazon DynamoDb Query command.+--+-- See: @http:\/\/docs.aws.amazon.com\/amazondynamodb\/latest\/APIReference\/API_Query.html@+----------------------------------------------------------------------------++module Aws.DynamoDb.Commands.Query+ ( Query (..)+ , Slice (..)+ , query+ , QueryResponse (..)+ ) where++-------------------------------------------------------------------------------+import Control.Applicative+import Data.Aeson+import Data.Default+import Data.Maybe+import qualified Data.Text as T+import Data.Typeable+import qualified Data.Vector as V+-------------------------------------------------------------------------------+import Aws.Core+import Aws.DynamoDb.Core+-------------------------------------------------------------------------------+++-------------------------------------------------------------------------------+-- | 'Slice' is the primary constraint in a 'Query' command, per AWS+-- requirements.+--+-- All 'Query' commands must specify a hash attribute via 'DEq' and+-- optionally provide a secondary range attribute.+data Slice = Slice {+ sliceHash :: Attribute+ -- ^ Hash value of the primary key or index being used+ , sliceCond :: Maybe Condition+ -- ^ An optional condition specified on the range component, if+ -- present, of the primary key or index being used.+ } deriving (Eq,Show,Read,Ord,Typeable)++++-- | A Query command that uses primary keys for an expedient scan.+data Query = Query {+ qTableName :: T.Text+ -- ^ Required.+ , qKeyConditions :: Slice+ -- ^ Required. Hash or hash-range main condition.+ , qFilter :: Conditions+ -- ^ Whether to filter results before returning to client+ , qStartKey :: Maybe [Attribute]+ -- ^ Exclusive start key to resume a previous query.+ , qLimit :: Maybe Int+ -- ^ Whether to limit result set size+ , qForwardScan :: Bool+ -- ^ Set to False for descending results+ , qSelect :: QuerySelect+ -- ^ What to return from 'Query'+ , qRetCons :: ReturnConsumption+ , qIndex :: Maybe T.Text+ -- ^ Whether to use a secondary/global index+ , qConsistent :: Bool+ } deriving (Eq,Show,Read,Ord,Typeable)+++-------------------------------------------------------------------------------+instance ToJSON Query where+ toJSON Query{..} = object $+ catMaybes+ [ (("ExclusiveStartKey" .= ) . attributesJson) <$> qStartKey+ , ("Limit" .= ) <$> qLimit+ , ("IndexName" .= ) <$> qIndex+ ] +++ conditionsJson "QueryFilter" qFilter +++ querySelectJson qSelect +++ [ "ScanIndexForward" .= qForwardScan+ , "TableName".= qTableName+ , "KeyConditions" .= sliceJson qKeyConditions+ , "ReturnConsumedCapacity" .= qRetCons+ , "ConsistentRead" .= qConsistent+ ]+++-------------------------------------------------------------------------------+-- | Construct a minimal 'Query' request.+query+ :: T.Text+ -- ^ Table name+ -> Slice+ -- ^ Primary key slice for query+ -> Query+query tn sl = Query tn sl def Nothing Nothing True def def Nothing False+++-- | Response to a 'Query' query.+data QueryResponse = QueryResponse {+ qrItems :: V.Vector Item+ , qrLastKey :: Maybe [Attribute]+ , qrCount :: Int+ , qrScanned :: Int+ , qrConsumed :: Maybe ConsumedCapacity+ } deriving (Eq,Show,Read,Ord)+++instance FromJSON QueryResponse where+ parseJSON (Object v) = QueryResponse+ <$> v .:? "Items" .!= V.empty+ <*> ((do o <- v .: "LastEvaluatedKey"+ Just <$> parseAttributeJson o)+ <|> pure Nothing)+ <*> v .: "Count"+ <*> v .: "ScannedCount"+ <*> v .:? "ConsumedCapacity"+ parseJSON _ = fail "QueryResponse must be an object."+++instance Transaction Query QueryResponse+++instance SignQuery Query where+ type ServiceConfiguration Query = DdbConfiguration+ signQuery gi = ddbSignQuery "Query" gi+++instance ResponseConsumer r QueryResponse where+ type ResponseMetadata QueryResponse = DdbResponse+ responseConsumer _ ref resp = ddbResponseConsumer ref resp+++instance AsMemoryResponse QueryResponse where+ type MemoryResponse QueryResponse = QueryResponse+ loadToMemory = return+++instance ListResponse QueryResponse Item where+ listResponse = V.toList . qrItems+++instance IteratedTransaction Query QueryResponse where+ nextIteratedRequest request response = case qrLastKey response of+ Nothing -> Nothing+ key -> Just request { qStartKey = key }+++sliceJson :: Slice -> Value+sliceJson Slice{..} = object (map conditionJson cs)+ where+ cs = maybe [] return sliceCond ++ [hashCond]+ hashCond = Condition (attrName sliceHash) (DEq (attrVal sliceHash))
+ Aws/DynamoDb/Commands/Scan.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynamoDb.Commands.Scan+-- Copyright : Soostone Inc+-- License : BSD3+--+-- Maintainer : Ozgun Ataman <ozgun.ataman@soostone.com>+-- Stability : experimental+--+-- Implementation of Amazon DynamoDb Scan command.+--+-- See: @http:\/\/docs.aws.amazon.com\/amazondynamodb\/latest\/APIReference\/API_Scan.html@+----------------------------------------------------------------------------++module Aws.DynamoDb.Commands.Scan+ ( Scan (..)+ , scan+ , ScanResponse (..)+ ) where++-------------------------------------------------------------------------------+import Control.Applicative+import Data.Aeson+import Data.Default+import Data.Maybe+import qualified Data.Text as T+import Data.Typeable+import qualified Data.Vector as V+-------------------------------------------------------------------------------+import Aws.Core+import Aws.DynamoDb.Core+-------------------------------------------------------------------------------+++-- | A Scan command that uses primary keys for an expedient scan.+data Scan = Scan {+ sTableName :: T.Text+ -- ^ Required.+ , sFilter :: Conditions+ -- ^ Whether to filter results before returning to client+ , sStartKey :: Maybe [Attribute]+ -- ^ Exclusive start key to resume a previous query.+ , sLimit :: Maybe Int+ -- ^ Whether to limit result set size+ , sSelect :: QuerySelect+ -- ^ What to return from 'Scan'+ , sRetCons :: ReturnConsumption+ , sSegment :: Int+ -- ^ Segment number, starting at 0, for parallel queries.+ , sTotalSegments :: Int+ -- ^ Total number of parallel segments. 1 means sequential scan.+ } deriving (Eq,Show,Read,Ord,Typeable)+++-- | Construct a minimal 'Scan' request.+scan :: T.Text -- ^ Table name+ -> Scan+scan tn = Scan tn def Nothing Nothing def def 0 1+++-- | Response to a 'Scan' query.+data ScanResponse = ScanResponse {+ srItems :: V.Vector Item+ , srLastKey :: Maybe [Attribute]+ , srCount :: Int+ , srScanned :: Int+ , srConsumed :: Maybe ConsumedCapacity+ } deriving (Eq,Show,Read,Ord)+++-------------------------------------------------------------------------------+instance ToJSON Scan where+ toJSON Scan{..} = object $+ catMaybes+ [ (("ExclusiveStartKey" .= ) . attributesJson) <$> sStartKey+ , ("Limit" .= ) <$> sLimit+ ] +++ conditionsJson "ScanFilter" sFilter +++ querySelectJson sSelect +++ [ "TableName".= sTableName+ , "ReturnConsumedCapacity" .= sRetCons+ , "Segment" .= sSegment+ , "TotalSegments" .= sTotalSegments+ ]+++instance FromJSON ScanResponse where+ parseJSON (Object v) = ScanResponse+ <$> v .:? "Items" .!= V.empty+ <*> ((do o <- v .: "LastEvaluatedKey"+ Just <$> parseAttributeJson o)+ <|> pure Nothing)+ <*> v .: "Count"+ <*> v .: "ScannedCount"+ <*> v .:? "ConsumedCapacity"+ parseJSON _ = fail "ScanResponse must be an object."+++instance Transaction Scan ScanResponse+++instance SignQuery Scan where+ type ServiceConfiguration Scan = DdbConfiguration+ signQuery gi = ddbSignQuery "Scan" gi+++instance ResponseConsumer r ScanResponse where+ type ResponseMetadata ScanResponse = DdbResponse+ responseConsumer _ ref resp = ddbResponseConsumer ref resp+++instance AsMemoryResponse ScanResponse where+ type MemoryResponse ScanResponse = ScanResponse+ loadToMemory = return++instance ListResponse ScanResponse Item where+ listResponse = V.toList . srItems++instance IteratedTransaction Scan ScanResponse where+ nextIteratedRequest request response =+ case srLastKey response of+ Nothing -> Nothing+ key -> Just request { sStartKey = key }
Aws/DynamoDb/Commands/Table.hs view
@@ -1,104 +1,121 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveGeneric #-}-module Aws.DynamoDb.Commands.Table(- -- * Commands- CreateTable(..)- , CreateTableResult(..)- , DescribeTable(..)- , DescribeTableResult(..)- , UpdateTable(..)- , UpdateTableResult(..)- , DeleteTable(..)- , DeleteTableResult(..)- , ListTables(..)- , ListTablesResult(..)+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies #-} - -- * Data passed in the commands- , KeyAttributeType(..)- , KeyAttributeDefinition(..)- , KeySchema(..)- , Projection(..)- , LocalSecondaryIndex(..)- , LocalSecondaryIndexStatus(..)- , ProvisionedThroughput(..)- , ProvisionedThroughputStatus(..)- , GlobalSecondaryIndex(..)- , GlobalSecondaryIndexStatus(..)- , GlobalSecondaryIndexUpdate(..)- , TableDescription(..)-) where+module Aws.DynamoDb.Commands.Table+ ( -- * Commands+ CreateTable(..)+ , createTable+ , CreateTableResult(..)+ , DescribeTable(..)+ , DescribeTableResult(..)+ , UpdateTable(..)+ , UpdateTableResult(..)+ , DeleteTable(..)+ , DeleteTableResult(..)+ , ListTables(..)+ , ListTablesResult(..) -import Aws.Core-import Aws.DynamoDb.Core+ -- * Data passed in the commands+ , AttributeType(..)+ , AttributeDefinition(..)+ , KeySchema(..)+ , Projection(..)+ , LocalSecondaryIndex(..)+ , LocalSecondaryIndexStatus(..)+ , ProvisionedThroughput(..)+ , ProvisionedThroughputStatus(..)+ , GlobalSecondaryIndex(..)+ , GlobalSecondaryIndexStatus(..)+ , GlobalSecondaryIndexUpdate(..)+ , TableDescription(..)+ ) where++------------------------------------------------------------------------------- import Control.Applicative-import Data.Aeson ((.=), (.:), (.!=), (.:?))-import qualified Data.Aeson as A-import qualified Data.Aeson.Types as A-import Data.Char (toUpper)+import Data.Aeson ((.!=), (.:), (.:?), (.=))+import qualified Data.Aeson as A+import qualified Data.Aeson.Types as A+import Data.Char (toUpper)+import qualified Data.HashMap.Strict as M+import qualified Data.Text as T import Data.Time import Data.Time.Clock.POSIX-import qualified Data.Text as T+import Data.Typeable import qualified Data.Vector as V-import qualified Data.HashMap.Strict as M-import GHC.Generics (Generic)+import GHC.Generics (Generic)+-------------------------------------------------------------------------------+import Aws.Core+import Aws.DynamoDb.Core+------------------------------------------------------------------------------- + capitalizeOpt :: A.Options-capitalizeOpt = A.defaultOptions { A.fieldLabelModifier = \x -> case x of- (c:cs) -> toUpper c : cs- [] -> []- }+capitalizeOpt = A.defaultOptions+ { A.fieldLabelModifier = \x -> case x of+ (c:cs) -> toUpper c : cs+ [] -> []+ } dropOpt :: Int -> A.Options dropOpt d = A.defaultOptions { A.fieldLabelModifier = drop d } --- | The type of a key attribute that appears in the table key or as a key in one of the indices.-data KeyAttributeType = AttrStringT | AttrNumberT | AttrBinaryT- deriving (Show, Eq, Enum, Bounded, Generic)-instance A.ToJSON KeyAttributeType where- toJSON AttrStringT = A.String "S"- toJSON AttrNumberT = A.String "N"- toJSON AttrBinaryT = A.String "B"-instance A.FromJSON KeyAttributeType where+-- | The type of a key attribute that appears in the table key or as a+-- key in one of the indices.+data AttributeType = AttrString | AttrNumber | AttrBinary+ deriving (Show, Read, Ord, Typeable, Eq, Enum, Bounded, Generic)++instance A.ToJSON AttributeType where+ toJSON AttrString = A.String "S"+ toJSON AttrNumber = A.String "N"+ toJSON AttrBinary = A.String "B"++instance A.FromJSON AttributeType where parseJSON (A.String str) = case str of- "S" -> return AttrStringT- "N" -> return AttrNumberT- "B" -> return AttrBinaryT+ "S" -> return AttrString+ "N" -> return AttrNumber+ "B" -> return AttrBinary _ -> fail $ "Invalid attribute type " ++ T.unpack str parseJSON _ = fail "Attribute type must be a string" -- | A key attribute that appears in the table key or as a key in one of the indices.-data KeyAttributeDefinition- = KeyAttributeDefinition {- attributeName :: T.Text- , attributeType :: KeyAttributeType- }- deriving (Show, Generic)-instance A.ToJSON KeyAttributeDefinition where+data AttributeDefinition = AttributeDefinition {+ attributeName :: T.Text+ , attributeType :: AttributeType+ } deriving (Eq,Read,Ord,Show,Typeable,Generic)++instance A.ToJSON AttributeDefinition where toJSON = A.genericToJSON capitalizeOpt-instance A.FromJSON KeyAttributeDefinition where++instance A.FromJSON AttributeDefinition where parseJSON = A.genericParseJSON capitalizeOpt -- | The key schema can either be a hash of a single attribute name or a hash attribute name -- and a range attribute name.-data KeySchema = KeyHashOnly T.Text- | KeyHashAndRange T.Text T.Text- deriving (Show)+data KeySchema = HashOnly T.Text+ | HashAndRange T.Text T.Text+ deriving (Eq,Read,Show,Ord,Typeable,Generic)++ instance A.ToJSON KeySchema where- toJSON (KeyHashOnly attr) - = A.Array $ V.fromList [ A.object [ "AttributeName" .= attr- , "KeyType" .= ("HASH" :: T.Text)+ toJSON (HashOnly a)+ = A.Array $ V.fromList [ A.object [ "AttributeName" .= a+ , "KeyType" .= (A.String "HASH") ] ]- toJSON (KeyHashAndRange hash range) ++ toJSON (HashAndRange hash range) = A.Array $ V.fromList [ A.object [ "AttributeName" .= hash- , "KeyType" .= ("HASH" :: T.Text)+ , "KeyType" .= (A.String "HASH") ] , A.object [ "AttributeName" .= range- , "KeyType" .= ("RANGE" :: T.Text)+ , "KeyType" .= (A.String "RANGE") ] ]+ instance A.FromJSON KeySchema where parseJSON (A.Array v) = case V.length v of@@ -106,7 +123,7 @@ kt <- obj .: "KeyType" if kt /= ("HASH" :: T.Text) then fail "With only one key, the type must be HASH"- else KeyHashOnly <$> obj .: "AttributeName"+ else HashOnly <$> obj .: "AttributeName" 2 -> do hash <- A.parseJSON (v V.! 0) range <- A.parseJSON (v V.! 1)@@ -114,8 +131,8 @@ rkt <- range .: "KeyType" if hkt /= ("HASH" :: T.Text) || rkt /= ("RANGE" :: T.Text) then fail "With two keys, one must be HASH and the other RANGE"- else KeyHashAndRange <$> hash .: "AttributeName"- <*> range .: "AttributeName"+ else HashAndRange <$> hash .: "AttributeName"+ <*> range .: "AttributeName" _ -> fail "Key schema must have one or two entries" parseJSON _ = fail "Key schema must be an array" @@ -139,13 +156,14 @@ "INCLUDE" -> ProjectInclude <$> o .: "NonKeyAttributes" _ -> fail "Invalid projection type" parseJSON _ = fail "Projection must be an object"- --- | Describes a single local secondary index. The KeySchema MUST share the same hash key attribute--- as the parent table, only the range key can differ.++-- | Describes a single local secondary index. The KeySchema MUST+-- share the same hash key attribute as the parent table, only the+-- range key can differ. data LocalSecondaryIndex = LocalSecondaryIndex {- localIndexName :: T.Text- , localKeySchema :: KeySchema+ localIndexName :: T.Text+ , localKeySchema :: KeySchema , localProjection :: Projection } deriving (Show, Generic)@@ -157,11 +175,11 @@ -- | This is returned by AWS to describe the local secondary index. data LocalSecondaryIndexStatus = LocalSecondaryIndexStatus {- locStatusIndexName :: T.Text+ locStatusIndexName :: T.Text , locStatusIndexSizeBytes :: Integer- , locStatusItemCount :: Integer- , locStatusKeySchema :: KeySchema- , locStatusProjection :: Projection+ , locStatusItemCount :: Integer+ , locStatusKeySchema :: KeySchema+ , locStatusProjection :: Projection } deriving (Show, Generic) instance A.FromJSON LocalSecondaryIndexStatus where@@ -170,7 +188,7 @@ -- | The target provisioned throughput you are requesting for the table or global secondary index. data ProvisionedThroughput = ProvisionedThroughput {- readCapacityUnits :: Int+ readCapacityUnits :: Int , writeCapacityUnits :: Int } deriving (Show, Generic)@@ -182,11 +200,11 @@ -- | This is returned by AWS as the status of the throughput for a table or global secondary index. data ProvisionedThroughputStatus = ProvisionedThroughputStatus {- statusLastDecreaseDateTime :: UTCTime- , statusLastIncreaseDateTime :: UTCTime+ statusLastDecreaseDateTime :: UTCTime+ , statusLastIncreaseDateTime :: UTCTime , statusNumberOfDecreasesToday :: Int- , statusReadCapacityUnits :: Int- , statusWriteCapacityUnits :: Int+ , statusReadCapacityUnits :: Int+ , statusWriteCapacityUnits :: Int } deriving (Show, Generic) instance A.FromJSON ProvisionedThroughputStatus where@@ -201,9 +219,9 @@ -- | Describes a global secondary index. data GlobalSecondaryIndex = GlobalSecondaryIndex {- globalIndexName :: T.Text- , globalKeySchema :: KeySchema- , globalProjection :: Projection+ globalIndexName :: T.Text+ , globalKeySchema :: KeySchema+ , globalProjection :: Projection , globalProvisionedThroughput :: ProvisionedThroughput } deriving (Show, Generic)@@ -215,44 +233,46 @@ -- | This is returned by AWS to describe the status of a global secondary index. data GlobalSecondaryIndexStatus = GlobalSecondaryIndexStatus {- gStatusIndexName :: T.Text- , gStatusIndexSizeBytes :: Integer- , gStatusIndexStatus :: T.Text- , gStatusItemCount :: Integer- , gStatusKeySchema :: KeySchema- , gStatusProjection :: Projection+ gStatusIndexName :: T.Text+ , gStatusIndexSizeBytes :: Integer+ , gStatusIndexStatus :: T.Text+ , gStatusItemCount :: Integer+ , gStatusKeySchema :: KeySchema+ , gStatusProjection :: Projection , gStatusProvisionedThroughput :: ProvisionedThroughputStatus } deriving (Show, Generic) instance A.FromJSON GlobalSecondaryIndexStatus where parseJSON = A.genericParseJSON $ dropOpt 7 --- | This is used to request a change in the provisioned throughput of a global secondary index as--- part of an 'UpdateTable' operation.+-- | This is used to request a change in the provisioned throughput of+-- a global secondary index as part of an 'UpdateTable' operation. data GlobalSecondaryIndexUpdate = GlobalSecondaryIndexUpdate {- gUpdateIndexName :: T.Text+ gUpdateIndexName :: T.Text , gUpdateProvisionedThroughput :: ProvisionedThroughput } deriving (Show, Generic) instance A.ToJSON GlobalSecondaryIndexUpdate where toJSON gi = A.object ["Update" .= A.genericToJSON (dropOpt 7) gi] --- | This describes the table and is the return value from AWS for all the table-related commands.+-- | This describes the table and is the return value from AWS for all+-- the table-related commands. data TableDescription = TableDescription {- rTableName :: T.Text- , rTableSizeBytes :: Integer- , rTableStatus :: T.Text -- ^ one of CREATING, UPDATING, DELETING, ACTIVE- , rCreationDateTime :: UTCTime- , rItemCount :: Integer- , rAttributeDefinitions :: [KeyAttributeDefinition]- , rKeySchema :: KeySchema- , rProvisionedThroughput :: ProvisionedThroughputStatus- , rLocalSecondaryIndexes :: [LocalSecondaryIndexStatus]+ rTableName :: T.Text+ , rTableSizeBytes :: Integer+ , rTableStatus :: T.Text -- ^ one of CREATING, UPDATING, DELETING, ACTIVE+ , rCreationDateTime :: UTCTime+ , rItemCount :: Integer+ , rAttributeDefinitions :: [AttributeDefinition]+ , rKeySchema :: KeySchema+ , rProvisionedThroughput :: ProvisionedThroughputStatus+ , rLocalSecondaryIndexes :: [LocalSecondaryIndexStatus] , rGlobalSecondaryIndexes :: [GlobalSecondaryIndexStatus] } deriving (Show, Generic)+ instance A.FromJSON TableDescription where parseJSON = A.withObject "Table must be an object" $ \o -> do t <- case (M.lookup "Table" o, M.lookup "TableDescription" o) of@@ -269,11 +289,11 @@ <*> t .: "ProvisionedThroughput" <*> t .:? "LocalSecondaryIndexes" .!= [] <*> t .:? "GlobalSecondaryIndexes" .!= []- + {- Can't derive these instances onto the return values instance ResponseConsumer r TableDescription where type ResponseMetadata TableDescription = DyMetadata- responseConsumer _ _ = dyResponseConsumer+ responseConsumer _ _ = ddbResponseConsumer instance AsMemoryResponse TableDescription where type MemoryResponse TableDescription = TableDescription loadToMemory = return@@ -283,16 +303,24 @@ --- Commands ------------------------------------------------------------------------------- -data CreateTable- = CreateTable {- createTableName :: T.Text- , createAttributeDefinitions :: [KeyAttributeDefinition] -- ^ only attributes appearing in a key must be listed here- , createKeySchema :: KeySchema- , createProvisionedThroughput :: ProvisionedThroughput- , createLocalSecondaryIndexes :: [LocalSecondaryIndex] -- ^ at most 5 local secondary indices are allowed- , createGlobalSecondaryIndexes :: [GlobalSecondaryIndex]- }- deriving (Show, Generic)+data CreateTable = CreateTable {+ createTableName :: T.Text+ , createAttributeDefinitions :: [AttributeDefinition]+ -- ^ only attributes appearing in a key must be listed here+ , createKeySchema :: KeySchema+ , createProvisionedThroughput :: ProvisionedThroughput+ , createLocalSecondaryIndexes :: [LocalSecondaryIndex]+ -- ^ at most 5 local secondary indices are allowed+ , createGlobalSecondaryIndexes :: [GlobalSecondaryIndex]+ } deriving (Show, Generic)++createTable :: T.Text -- ^ Table name+ -> [AttributeDefinition]+ -> KeySchema+ -> ProvisionedThroughput+ -> CreateTable+createTable tn ad ks p = CreateTable tn ad ks p [] []+ instance A.ToJSON CreateTable where toJSON ct = A.object $ m ++ lindex ++ gindex where@@ -308,20 +336,22 @@ gindex = if null (createGlobalSecondaryIndexes ct) then [] else [ "GlobalSecondaryIndexes" .= createGlobalSecondaryIndexes ct ]+ --instance A.ToJSON CreateTable where -- toJSON = A.genericToJSON $ dropOpt 6- --- | ServiceConfiguration: 'DyConfiguration'+++-- | ServiceConfiguration: 'DdbConfiguration' instance SignQuery CreateTable where- type ServiceConfiguration CreateTable = DyConfiguration- signQuery = dySignQuery "CreateTable"+ type ServiceConfiguration CreateTable = DdbConfiguration+ signQuery = ddbSignQuery "CreateTable" newtype CreateTableResult = CreateTableResult { ctStatus :: TableDescription } deriving (Show, A.FromJSON) -- ResponseConsumer and AsMemoryResponse can't be derived instance ResponseConsumer r CreateTableResult where- type ResponseMetadata CreateTableResult = DyMetadata- responseConsumer _ _ = dyResponseConsumer+ type ResponseMetadata CreateTableResult = DdbResponse+ responseConsumer _ = ddbResponseConsumer instance AsMemoryResponse CreateTableResult where type MemoryResponse CreateTableResult = TableDescription loadToMemory = return . ctStatus@@ -330,23 +360,23 @@ data DescribeTable = DescribeTable {- dTableName :: T.Text + dTableName :: T.Text } deriving (Show, Generic) instance A.ToJSON DescribeTable where toJSON = A.genericToJSON $ dropOpt 1 --- | ServiceConfiguration: 'DyConfiguration'+-- | ServiceConfiguration: 'DdbConfiguration' instance SignQuery DescribeTable where- type ServiceConfiguration DescribeTable = DyConfiguration- signQuery = dySignQuery "DescribeTable"+ type ServiceConfiguration DescribeTable = DdbConfiguration+ signQuery = ddbSignQuery "DescribeTable" newtype DescribeTableResult = DescribeTableResult { dtStatus :: TableDescription } deriving (Show, A.FromJSON) -- ResponseConsumer can't be derived instance ResponseConsumer r DescribeTableResult where- type ResponseMetadata DescribeTableResult = DyMetadata- responseConsumer _ _ = dyResponseConsumer+ type ResponseMetadata DescribeTableResult = DdbResponse+ responseConsumer _ = ddbResponseConsumer instance AsMemoryResponse DescribeTableResult where type MemoryResponse DescribeTableResult = TableDescription loadToMemory = return . dtStatus@@ -355,25 +385,25 @@ data UpdateTable = UpdateTable {- updateTableName :: T.Text- , updateProvisionedThroughput :: ProvisionedThroughput+ updateTableName :: T.Text+ , updateProvisionedThroughput :: ProvisionedThroughput , updateGlobalSecondaryIndexUpdates :: [GlobalSecondaryIndexUpdate] } deriving (Show, Generic) instance A.ToJSON UpdateTable where toJSON = A.genericToJSON $ dropOpt 6- --- | ServiceConfiguration: 'DyConfiguration'++-- | ServiceConfiguration: 'DdbConfiguration' instance SignQuery UpdateTable where- type ServiceConfiguration UpdateTable = DyConfiguration- signQuery = dySignQuery "UpdateTable"+ type ServiceConfiguration UpdateTable = DdbConfiguration+ signQuery = ddbSignQuery "UpdateTable" newtype UpdateTableResult = UpdateTableResult { uStatus :: TableDescription } deriving (Show, A.FromJSON) -- ResponseConsumer can't be derived instance ResponseConsumer r UpdateTableResult where- type ResponseMetadata UpdateTableResult = DyMetadata- responseConsumer _ _ = dyResponseConsumer+ type ResponseMetadata UpdateTableResult = DdbResponse+ responseConsumer _ = ddbResponseConsumer instance AsMemoryResponse UpdateTableResult where type MemoryResponse UpdateTableResult = TableDescription loadToMemory = return . uStatus@@ -388,17 +418,17 @@ instance A.ToJSON DeleteTable where toJSON = A.genericToJSON $ dropOpt 6 --- | ServiceConfiguration: 'DyConfiguration'+-- | ServiceConfiguration: 'DdbConfiguration' instance SignQuery DeleteTable where- type ServiceConfiguration DeleteTable = DyConfiguration- signQuery = dySignQuery "DeleteTable"+ type ServiceConfiguration DeleteTable = DdbConfiguration+ signQuery = ddbSignQuery "DeleteTable" newtype DeleteTableResult = DeleteTableResult { dStatus :: TableDescription } deriving (Show, A.FromJSON) -- ResponseConsumer can't be derived instance ResponseConsumer r DeleteTableResult where- type ResponseMetadata DeleteTableResult = DyMetadata- responseConsumer _ _ = dyResponseConsumer+ type ResponseMetadata DeleteTableResult = DdbResponse+ responseConsumer _ = ddbResponseConsumer instance AsMemoryResponse DeleteTableResult where type MemoryResponse DeleteTableResult = TableDescription loadToMemory = return . dStatus@@ -410,23 +440,23 @@ deriving (Show) instance A.ToJSON ListTables where toJSON _ = A.object []--- | ServiceConfiguration: 'DyConfiguration'+-- | ServiceConfiguration: 'DdbConfiguration' instance SignQuery ListTables where- type ServiceConfiguration ListTables = DyConfiguration- signQuery = dySignQuery "ListTables"+ type ServiceConfiguration ListTables = DdbConfiguration+ signQuery = ddbSignQuery "ListTables" newtype ListTablesResult = ListTablesResult {- tableNames :: [T.Text] + tableNames :: [T.Text] } deriving (Show, Generic) instance A.FromJSON ListTablesResult where parseJSON = A.genericParseJSON capitalizeOpt instance ResponseConsumer r ListTablesResult where- type ResponseMetadata ListTablesResult = DyMetadata- responseConsumer _ _ = dyResponseConsumer+ type ResponseMetadata ListTablesResult = DdbResponse+ responseConsumer _ = ddbResponseConsumer instance AsMemoryResponse ListTablesResult where- type MemoryResponse ListTablesResult = [T.Text] + type MemoryResponse ListTablesResult = [T.Text] loadToMemory = return . tableNames instance Transaction ListTables ListTablesResult
+ Aws/DynamoDb/Commands/UpdateItem.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynamoDb.Commands.UpdateItem+-- Copyright : Soostone Inc+-- License : BSD3+--+-- Maintainer : Ozgun Ataman <ozgun.ataman@soostone.com>+-- Stability : experimental+--+--+----------------------------------------------------------------------------++module Aws.DynamoDb.Commands.UpdateItem where++-------------------------------------------------------------------------------+import Control.Applicative+import Data.Aeson+import Data.Default+import qualified Data.Text as T+-------------------------------------------------------------------------------+import Aws.Core+import Aws.DynamoDb.Core+-------------------------------------------------------------------------------+++-- | An @UpdateItem@ request.+data UpdateItem = UpdateItem {+ uiTable :: T.Text+ , uiKey :: PrimaryKey+ , uiUpdates :: [AttributeUpdate]+ , uiExpect :: Conditions+ -- ^ Conditional update - see DynamoDb documentation+ , uiReturn :: UpdateReturn+ , uiRetCons :: ReturnConsumption+ , uiRetMet :: ReturnItemCollectionMetrics+ } deriving (Eq,Show,Read,Ord)+++-------------------------------------------------------------------------------+-- | Construct a minimal 'UpdateItem' request.+updateItem+ :: T.Text -- ^ Table name+ -> PrimaryKey -- ^ Primary key for item+ -> [AttributeUpdate] -- ^ Updates for this item+ -> UpdateItem+updateItem tn key ups = UpdateItem tn key ups def def def def+++type AttributeUpdates = [AttributeUpdate]+++data AttributeUpdate = AttributeUpdate {+ auAttr :: Attribute+ -- ^ Attribute key-value+ , auAction :: UpdateAction+ -- ^ Type of update operation.+ } deriving (Eq,Show,Read,Ord)+++instance DynSize AttributeUpdate where+ dynSize (AttributeUpdate a _) = dynSize a++-------------------------------------------------------------------------------+-- | Shorthand for the 'AttributeUpdate' constructor. Defaults to PUT+-- for the update action.+au :: Attribute -> AttributeUpdate+au a = AttributeUpdate a def+++instance ToJSON AttributeUpdates where+ toJSON = object . map mk+ where+ mk AttributeUpdate{..} = (attrName auAttr) .= object+ ["Value" .= (attrVal auAttr), "Action" .= auAction]+++-------------------------------------------------------------------------------+-- | Type of attribute update to perform.+--+-- See AWS docs at:+--+-- @http:\/\/docs.aws.amazon.com\/amazondynamodb\/latest\/APIReference\/API_UpdateItem.html@+data UpdateAction+ = UPut -- ^ Simpley write, overwriting any previous value+ | UAdd -- ^ Numerical add or add to set.+ | UDelete -- ^ Empty value: remove; Set value: Subtract from set.+ deriving (Eq,Show,Read,Ord)+++instance ToJSON UpdateAction where+ toJSON UPut = String "PUT"+ toJSON UAdd = String "ADD"+ toJSON UDelete = String "DELETE"+++instance Default UpdateAction where+ def = UPut+++instance ToJSON UpdateItem where+ toJSON UpdateItem{..} =+ object $ expectsJson uiExpect +++ [ "TableName" .= uiTable+ , "Key" .= uiKey+ , "AttributeUpdates" .= uiUpdates+ , "ReturnValues" .= uiReturn+ , "ReturnConsumedCapacity" .= uiRetCons+ , "ReturnItemCollectionMetrics" .= uiRetMet+ ]+++data UpdateItemResponse = UpdateItemResponse {+ uirAttrs :: Maybe Item+ -- ^ Old attributes, if requested+ , uirConsumed :: Maybe ConsumedCapacity+ -- ^ Amount of capacity consumed+ } deriving (Eq,Show,Read,Ord)++++instance Transaction UpdateItem UpdateItemResponse+++instance SignQuery UpdateItem where+ type ServiceConfiguration UpdateItem = DdbConfiguration+ signQuery gi = ddbSignQuery "UpdateItem" gi+++instance FromJSON UpdateItemResponse where+ parseJSON (Object v) = UpdateItemResponse+ <$> v .:? "Attributes"+ <*> v .:? "ConsumedCapacity"+ parseJSON _ = fail "UpdateItemResponse expected a JSON object"+++instance ResponseConsumer r UpdateItemResponse where+ type ResponseMetadata UpdateItemResponse = DdbResponse+ responseConsumer _ ref resp = ddbResponseConsumer ref resp+++instance AsMemoryResponse UpdateItemResponse where+ type MemoryResponse UpdateItemResponse = UpdateItemResponse+ loadToMemory = return++++++++
Aws/DynamoDb/Core.hs view
@@ -1,126 +1,1136 @@-module Aws.DynamoDb.Core where--import Aws.Core-import qualified Control.Exception as C-import Control.Monad.Trans.Resource (throwM)-import Crypto.Hash-import Data.Byteable-import qualified Data.Aeson as A-import qualified Data.ByteString as B-import qualified Data.ByteString.Base16 as Base16-import Data.Conduit-import qualified Data.Conduit.Attoparsec as Atto-import Data.Monoid-import Data.Typeable-import qualified Network.HTTP.Conduit as HTTP-import qualified Network.HTTP.Types as HTTP--type ErrorCode = String--data DyError- = DyError {- dyStatusCode :: HTTP.Status- , dyErrorCode :: ErrorCode- , dyErrorMessage :: String- }- deriving (Show, Typeable)--instance C.Exception DyError--data DyMetadata = DyMetadata- deriving (Show, Typeable)--instance Loggable DyMetadata where- toLogText DyMetadata = "DynamoDB"--instance Monoid DyMetadata where- mempty = DyMetadata- DyMetadata `mappend` DyMetadata = DyMetadata--data DyConfiguration qt- = DyConfiguration {- dyProtocol :: Protocol- , dyHost :: B.ByteString- , dyPort :: Int- , dyRegion :: B.ByteString- }- deriving (Show)--instance DefaultServiceConfiguration (DyConfiguration NormalQuery) where- defServiceConfig = dyHttp dyUsEast- debugServiceConfig = dyLocal--dyUsEast :: (B.ByteString, B.ByteString)-dyUsEast = ("us-east-1", "dynamodb.us-east-1.amazonaws.com")--dyHttp :: (B.ByteString, B.ByteString) -> DyConfiguration qt-dyHttp (region, endpoint) = DyConfiguration HTTP endpoint (defaultPort HTTP) region--dyHttps :: (B.ByteString, B.ByteString) -> DyConfiguration qt-dyHttps (region, endpoint) = DyConfiguration HTTPS endpoint (defaultPort HTTPS) region--dyLocal :: DyConfiguration qt-dyLocal = DyConfiguration HTTP "localhost" 8000 "local"--dyApiVersion :: B.ByteString-dyApiVersion = "DynamoDB_20120810."--dySignQuery :: A.ToJSON a => B.ByteString -> a -> DyConfiguration qt -> SignatureData -> SignedQuery-dySignQuery target body di sd- = SignedQuery {- sqMethod = Post- , sqProtocol = dyProtocol di- , sqHost = dyHost di- , sqPort = dyPort di- , sqPath = "/"- , sqQuery = []- , sqDate = Just $ signatureTime sd- , sqAuthorization = Just auth- , sqContentType = Just "application/x-amz-json-1.0"- , sqContentMd5 = Nothing- , sqAmzHeaders = [ ("X-Amz-Target", dyApiVersion <> target)- , ("X-Amz-Date", sigTime)- ]- , sqOtherHeaders = []- , sqBody = Just $ HTTP.RequestBodyLBS bodyLBS- , sqStringToSign = canonicalRequest- }- where- sigTime = fmtTime "%Y%m%dT%H%M%SZ" $ signatureTime sd-- bodyLBS = A.encode body- bodyHash = Base16.encode $ toBytes (hashlazy bodyLBS :: Digest SHA256)-- canonicalRequest = B.concat [ "POST\n"- , "/\n"- , "\n" -- query string- , "content-type:application/x-amz-json-1.0\n"- , "host:"- , dyHost di- , "\n"- , "x-amz-date:"- , sigTime- , "\n"- , "x-amz-target:"- , dyApiVersion- , target- , "\n"- , "\n" -- end headers- , "content-type;host;x-amz-date;x-amz-target\n"- , bodyHash- ]-- auth = authorizationV4 sd HmacSHA256 (dyRegion di) "dynamodb"- "content-type;host;x-amz-date;x-amz-target"- canonicalRequest--dyResponseConsumer :: A.FromJSON a- => HTTPResponseConsumer a-dyResponseConsumer resp = do- val <- HTTP.responseBody resp $$+- Atto.sinkParser A.json'- case HTTP.responseStatus resp of- (HTTP.Status{HTTP.statusCode=200}) -> do- case A.fromJSON val of- A.Success a -> return a- A.Error err -> throwM $ DyError (HTTP.responseStatus resp) "" err- _ -> throwM $ DyError (HTTP.responseStatus resp) "" (show val)+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+-----------------------------------------------------------------------------+-- |+-- Module : Aws.DynamoDb.Core+-- Copyright : Soostone Inc, Chris Allen+-- License : BSD3+--+-- Maintainer : Ozgun Ataman, Chris Allen+-- Stability : experimental+--+-- Shared types and utilities for DyanmoDb functionality.+----------------------------------------------------------------------------++module Aws.DynamoDb.Core+ (+ -- * Configuration and Regions+ Region (..)+ , ddbLocal+ , ddbUsEast1+ , ddbUsWest1+ , ddbUsWest2+ , ddbEuWest1+ , ddbApNe1+ , ddbApSe1+ , ddbApSe2+ , ddbSaEast1+ , DdbConfiguration (..)++ -- * DynamoDB values+ , DValue (..)++ -- * Converting to/from 'DValue'+ , DynVal(..)+ , toValue, fromValue+ , Bin (..)++ -- * Defining new 'DynVal' instances+ , DynData(..)+ , DynBinary(..), DynNumber(..), DynString(..)++ -- * Working with key/value pairs+ , Attribute (..)+ , parseAttributeJson+ , attributeJson+ , attributesJson++ , attrTuple+ , attr+ , attrAs+ , text, int, double+ , PrimaryKey (..)+ , hk+ , hrk++ -- * Working with objects (attribute collections)+ , Item+ , item+ , attributes++ -- * Common types used by operations+ , Conditions (..)+ , conditionsJson+ , expectsJson++ , Condition (..)+ , conditionJson+ , CondOp (..)+ , CondMerge (..)+ , ConsumedCapacity (..)+ , ReturnConsumption (..)+ , ItemCollectionMetrics (..)+ , ReturnItemCollectionMetrics (..)+ , UpdateReturn (..)+ , QuerySelect+ , querySelectJson++ -- * Size estimation+ , DynSize (..)+ , nullAttr++ -- * Responses & Errors+ , DdbResponse (..)+ , DdbErrCode (..)+ , shouldRetry+ , DdbError (..)++ -- * Internal Helpers+ , ddbSignQuery+ , AmazonError (..)+ , ddbResponseConsumer+ , ddbHttp+ , ddbHttps++ ) where+++-------------------------------------------------------------------------------+import Control.Applicative+import qualified Control.Exception as C+import Control.Monad+import Control.Monad.Trans+import Control.Monad.Trans.Resource (throwM)+import Crypto.Hash+import Data.Aeson+import qualified Data.Aeson as A+import Data.Aeson.Types (Pair, parseEither)+import qualified Data.Aeson.Types as A+import qualified Data.Attoparsec.Text as Atto+import Data.Byteable+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Base64 as Base64+import qualified Data.ByteString.Char8 as B+import Data.Conduit+import Data.Conduit.Attoparsec (sinkParser)+import Data.Default+import qualified Data.HashMap.Strict as HM+import Data.Int+import Data.IORef+import qualified Data.Map as M+import Data.Maybe+import Data.Monoid+import Data.Proxy+import Data.Scientific+import qualified Data.Serialize as Ser+import qualified Data.Set as S+import Data.String+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Time+import Data.Typeable+import Data.Word+import qualified Network.HTTP.Conduit as HTTP+import qualified Network.HTTP.Types as HTTP+import Safe+-------------------------------------------------------------------------------+import Aws.Core+-------------------------------------------------------------------------------++++-------------------------------------------------------------------------------+-- | Numeric values stored in DynamoDb. Only used in defining new+-- 'DynVal' instances.+newtype DynNumber = DynNumber { unDynNumber :: Scientific }+ deriving (Eq,Show,Read,Ord,Typeable)+++-------------------------------------------------------------------------------+-- | String values stored in DynamoDb. Only used in defining new+-- 'DynVal' instances.+newtype DynString = DynString { unDynString :: T.Text }+ deriving (Eq,Show,Read,Ord,Typeable)+++-------------------------------------------------------------------------------+-- | Binary values stored in DynamoDb. Only used in defining new+-- 'DynVal' instances.+newtype DynBinary = DynBinary { unDynBinary :: B.ByteString }+ deriving (Eq,Show,Read,Ord,Typeable)+++-------------------------------------------------------------------------------+-- | An internally used closed typeclass for values that have direct+-- DynamoDb representations. Based on AWS API, this is basically+-- numbers, strings and binary blobs.+--+-- This is here so that any 'DynVal' haskell value can automatically+-- be lifted to a list or a 'Set' without any instance code+-- duplication.+--+-- Do not try to create your own instances.+class Ord a => DynData a where+ fromData :: a -> DValue+ toData :: DValue -> Maybe a++instance DynData DynNumber where+ fromData (DynNumber i) = DNum i+ toData (DNum i) = Just $ DynNumber i+ toData _ = Nothing++instance DynData (S.Set DynNumber) where+ fromData set = DNumSet (S.map unDynNumber set)+ toData (DNumSet i) = Just $ S.map DynNumber i+ toData _ = Nothing++instance DynData DynString where+ fromData (DynString i) = DString i+ toData (DString i) = Just $ DynString i+ toData _ = Nothing++instance DynData (S.Set DynString) where+ fromData set = DStringSet (S.map unDynString set)+ toData (DStringSet i) = Just $ S.map DynString i+ toData _ = Nothing++instance DynData DynBinary where+ fromData (DynBinary i) = DBinary i+ toData (DBinary i) = Just $ DynBinary i+ toData _ = Nothing++instance DynData (S.Set DynBinary) where+ fromData set = DBinSet (S.map unDynBinary set)+ toData (DBinSet i) = Just $ S.map DynBinary i+ toData _ = Nothing++instance DynData DValue where+ fromData = id+ toData = Just+++-------------------------------------------------------------------------------+-- | Class of Haskell types that can be represented as DynamoDb values.+--+-- This is the conversion layer; instantiate this class for your own+-- types and then use the 'toValue' and 'fromValue' combinators to+-- convert in application code.+--+-- Each Haskell type instantiated with this class will map to a+-- DynamoDb-supported type that most naturally represents it.+class DynData (DynRep a) => DynVal a where++ -- | Which of the 'DynData' instances does this data type directly+ -- map to?+ type DynRep a++ -- | Convert to representation+ toRep :: a -> DynRep a++ -- | Convert from representation+ fromRep :: DynRep a -> Maybe a+++-------------------------------------------------------------------------------+-- | Any singular 'DynVal' can be upgraded to a list.+instance (DynData (DynRep [a]), DynVal a) => DynVal [a] where+ type DynRep [a] = S.Set (DynRep a)+ fromRep set = mapM fromRep $ S.toList set+ toRep as = S.fromList $ map toRep as+++-------------------------------------------------------------------------------+-- | Any singular 'DynVal' can be upgraded to a 'Set'.+instance (DynData (DynRep (S.Set a)), DynVal a, Ord a) => DynVal (S.Set a) where+ type DynRep (S.Set a) = S.Set (DynRep a)+ fromRep set = fmap S.fromList . mapM fromRep $ S.toList set+ toRep as = S.map toRep as+++instance DynVal DValue where+ type DynRep DValue = DValue+ fromRep = Just+ toRep = id+++instance DynVal Int where+ type DynRep Int = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Int8 where+ type DynRep Int8 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Int16 where+ type DynRep Int16 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Int32 where+ type DynRep Int32 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Int64 where+ type DynRep Int64 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Word8 where+ type DynRep Word8 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Word16 where+ type DynRep Word16 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Word32 where+ type DynRep Word32 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Word64 where+ type DynRep Word64 = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal Integer where+ type DynRep Integer = DynNumber+ fromRep (DynNumber i) = toIntegral i+ toRep i = DynNumber (fromIntegral i)+++instance DynVal T.Text where+ type DynRep T.Text = DynString+ fromRep (DynString i) = Just i+ toRep i = DynString i+++instance DynVal B.ByteString where+ type DynRep B.ByteString = DynBinary+ fromRep (DynBinary i) = Just i+ toRep i = DynBinary i+++instance DynVal Double where+ type DynRep Double = DynNumber+ fromRep (DynNumber i) = Just $ toRealFloat i+ toRep i = DynNumber (fromFloatDigits i)+++-------------------------------------------------------------------------------+-- | Encoded as number of days+instance DynVal Day where+ type DynRep Day = DynNumber+ fromRep (DynNumber i) = ModifiedJulianDay <$> (toIntegral i)+ toRep (ModifiedJulianDay i) = DynNumber (fromIntegral i)+++-------------------------------------------------------------------------------+-- | Losslessly encoded via 'Integer' picoseconds+instance DynVal UTCTime where+ type DynRep UTCTime = DynNumber+ fromRep num = fromTS <$> fromRep num+ toRep x = toRep (toTS x)+++-------------------------------------------------------------------------------+pico :: Integer+pico = 10 ^ (12 :: Integer)+++-------------------------------------------------------------------------------+dayPico :: Integer+dayPico = 86400 * pico+++-------------------------------------------------------------------------------+-- | Convert UTCTime to picoseconds+--+-- TODO: Optimize performance?+toTS :: UTCTime -> Integer+toTS (UTCTime (ModifiedJulianDay i) diff) = i' + diff'+ where+ diff' = floor (toRational diff * pico')+ pico' = toRational pico+ i' = i * dayPico+++-------------------------------------------------------------------------------+-- | Convert picoseconds to UTCTime+--+-- TODO: Optimize performance?+fromTS :: Integer -> UTCTime+fromTS i = UTCTime (ModifiedJulianDay days) diff+ where+ (days, secs) = i `divMod` dayPico+ diff = fromRational ((toRational secs) / toRational pico)+++-- | Encoded as 0 and 1.+instance DynVal Bool where+ type DynRep Bool = DynNumber+ fromRep (DynNumber i) = do+ (i' :: Int) <- toIntegral i+ case i' of+ 0 -> return False+ 1 -> return True+ _ -> Nothing+ toRep b = DynNumber (if b then 1 else 0)++++-- | Type wrapper for binary data to be written to DynamoDB. Wrap any+-- 'Serialize' instance in there and 'DynVal' will know how to+-- automatically handle conversions in binary form.+newtype Bin a = Bin a deriving (Eq,Show,Read,Ord)+++instance (Ser.Serialize a) => DynVal (Bin a) where+ type DynRep (Bin a) = DynBinary+ toRep (Bin i) = DynBinary (Ser.encode i)+ fromRep (DynBinary i) = either (const Nothing) (Just . Bin) $+ Ser.decode i++++-------------------------------------------------------------------------------+-- | Encode a Haskell value.+toValue :: DynVal a => a -> DValue+toValue a = fromData $ toRep a+++-------------------------------------------------------------------------------+-- | Decode a Haskell value.+fromValue :: DynVal a => DValue -> Maybe a+fromValue d = toData d >>= fromRep+++toIntegral :: (Integral a, RealFrac a1) => a1 -> Maybe a+toIntegral sc = Just $ floor sc++++-- | Value types natively recognized by DynamoDb. We pretty much+-- exactly reflect the AWS API onto Haskell types.+data DValue+ = DNum Scientific+ | DString T.Text+ | DBinary B.ByteString+ -- ^ Binary data will automatically be base64 marshalled.+ | DNumSet (S.Set Scientific)+ | DStringSet (S.Set T.Text)+ | DBinSet (S.Set B.ByteString)+ -- ^ Binary data will automatically be base64 marshalled.+ deriving (Eq,Show,Read,Ord,Typeable)+++instance IsString DValue where+ fromString t = DString (T.pack t)++-------------------------------------------------------------------------------+-- | Primary keys consist of either just a Hash key (mandatory) or a+-- hash key and a range key (optional).+data PrimaryKey = PrimaryKey {+ pkHash :: Attribute+ , pkRange :: Maybe Attribute+ } deriving (Read,Show,Ord,Eq,Typeable)+++-------------------------------------------------------------------------------+-- | Construct a hash-only primary key.+--+-- >>> hk "user-id" "ABCD"+--+-- >>> hk "user-id" (mkVal 23)+hk :: T.Text -> DValue -> PrimaryKey+hk k v = PrimaryKey (attr k v) Nothing+++-------------------------------------------------------------------------------+-- | Construct a hash-and-range primary key.+hrk :: T.Text -- ^ Hash key name+ -> DValue -- ^ Hash key value+ -> T.Text -- ^ Range key name+ -> DValue -- ^ Range key value+ -> PrimaryKey+hrk k v k2 v2 = PrimaryKey (attr k v) (Just (attr k2 v2))+++instance ToJSON PrimaryKey where+ toJSON (PrimaryKey h Nothing) = toJSON h+ toJSON (PrimaryKey h (Just r)) =+ let Object p1 = toJSON h+ Object p2 = toJSON r+ in Object (p1 `HM.union` p2)+++-- | A key-value pair+data Attribute = Attribute {+ attrName :: T.Text+ , attrVal :: DValue+ } deriving (Read,Show,Ord,Eq,Typeable)+++-- | Convert attribute to a tuple representation+attrTuple :: Attribute -> (T.Text, DValue)+attrTuple (Attribute a b) = (a,b)+++-- | Convenience function for constructing key-value pairs+attr :: DynVal a => T.Text -> a -> Attribute+attr k v = Attribute k (toValue v)+++-- | 'attr' with type witness to help with cases where you're manually+-- supplying values in code.+--+-- >> item [ attrAs text "name" "john" ]+attrAs :: DynVal a => Proxy a -> T.Text -> a -> Attribute+attrAs _ k v = attr k v+++-- | Type witness for 'Text'. See 'attrAs'.+text :: Proxy T.Text+text = Proxy+++-- | Type witness for 'Integer'. See 'attrAs'.+int :: Proxy Integer+int = Proxy+++-- | Type witness for 'Double'. See 'attrAs'.+double :: Proxy Double+double = Proxy+++-- | A DynamoDb object is simply a key-value dictionary.+type Item = M.Map T.Text DValue+++-------------------------------------------------------------------------------+-- | Pack a list of attributes into an Item.+item :: [Attribute] -> Item+item = M.fromList . map attrTuple+++-------------------------------------------------------------------------------+-- | Unpack an 'Item' into a list of attributes.+attributes :: M.Map T.Text DValue -> [Attribute]+attributes = map (\ (k, v) -> Attribute k v) . M.toList+++showT :: Show a => a -> T.Text+showT = T.pack . show++instance ToJSON DValue where+ toJSON (DNum i) = object ["N" .= showT i]+ toJSON (DString i) = object ["S" .= i]+ toJSON (DBinary i) = object ["B" .= (T.decodeUtf8 $ Base64.encode i)]+ toJSON (DNumSet i) = object ["NS" .= map showT (S.toList i)]+ toJSON (DStringSet i) = object ["SS" .= S.toList i]+ toJSON (DBinSet i) = object ["BS" .= map (T.decodeUtf8 . Base64.encode) (S.toList i)]+ toJSON x = error $ "aws: bug: DynamoDB can't handle " ++ show x+++instance FromJSON DValue where+ parseJSON o = do+ (obj :: [(T.Text, Value)]) <- M.toList `liftM` parseJSON o+ case obj of+ [("N", numStr)] -> DNum <$> parseScientific numStr+ [("S", str)] -> DString <$> parseJSON str+ [("B", bin)] -> do+ res <- (Base64.decode . T.encodeUtf8) <$> parseJSON bin+ either fail (return . DBinary) res+ [("NS", s)] -> do xs <- mapM parseScientific =<< parseJSON s+ return $ DNumSet $ S.fromList xs+ [("SS", s)] -> DStringSet <$> parseJSON s+ [("BS", s)] -> do+ xs <- mapM (either fail return . Base64.decode . T.encodeUtf8)+ =<< parseJSON s+ return $ DBinSet $ S.fromList xs++ x -> fail $ "aws: unknown dynamodb value: " ++ show x++ where+ parseScientific (String str) =+ case Atto.parseOnly Atto.scientific str of+ Left e -> fail ("parseScientific failed: " ++ e)+ Right a -> return a+ parseScientific (Number n) = return n+ parseScientific _ = fail "Unexpected JSON type in parseScientific"+++instance ToJSON Attribute where+ toJSON a = object $ [attributeJson a]+++-------------------------------------------------------------------------------+-- | Parse a JSON object that contains attributes+parseAttributeJson :: Value -> A.Parser [Attribute]+parseAttributeJson (Object v) = mapM conv $ HM.toList v+ where+ conv (k, o) = Attribute k <$> parseJSON o+parseAttributeJson _ = error "Attribute JSON must be an Object"+++-- | Convert into JSON object for AWS.+attributesJson :: [Attribute] -> Value+attributesJson as = object $ map attributeJson as+++-- | Convert into JSON pair+attributeJson :: Attribute -> Pair+attributeJson (Attribute nm v) = nm .= v+++-------------------------------------------------------------------------------+-- | Errors defined by AWS.+data DdbErrCode+ = AccessDeniedException+ | ConditionalCheckFailedException+ | IncompleteSignatureException+ | InvalidSignatureException+ | LimitExceededException+ | MissingAuthenticationTokenException+ | ProvisionedThroughputExceededException+ | ResourceInUseException+ | ResourceNotFoundException+ | ThrottlingException+ | ValidationException+ | RequestTooLarge+ | InternalFailure+ | InternalServerError+ | ServiceUnavailableException+ | SerializationException+ -- ^ Raised by AWS when the request JSON is missing fields or is+ -- somehow malformed.+ deriving (Read,Show,Eq,Typeable)+++-------------------------------------------------------------------------------+-- | Whether the action should be retried based on the received error.+shouldRetry :: DdbErrCode -> Bool+shouldRetry e = go e+ where+ go LimitExceededException = True+ go ProvisionedThroughputExceededException = True+ go ResourceInUseException = True+ go ThrottlingException = True+ go InternalFailure = True+ go InternalServerError = True+ go ServiceUnavailableException = True+ go _ = False+++-------------------------------------------------------------------------------+-- | Errors related to this library.+data DdbLibraryError+ = UnknownDynamoErrCode T.Text+ -- ^ A DynamoDB error code we do not know about.+ | JsonProtocolError Value T.Text+ -- ^ A JSON response we could not parse.+ deriving (Show,Eq,Typeable)+++-- | Potential errors raised by DynamoDB+data DdbError = DdbError {+ ddbStatusCode :: Int+ -- ^ 200 if successful, 400 for client errors and 500 for+ -- server-side errors.+ , ddbErrCode :: DdbErrCode+ , ddbErrMsg :: T.Text+ } deriving (Show,Eq,Typeable)+++instance C.Exception DdbError+instance C.Exception DdbLibraryError+++-- | Response metadata that is present in every DynamoDB response.+data DdbResponse = DdbResponse {+ ddbrCrc :: Maybe T.Text+ , ddbrMsgId :: Maybe T.Text+ }+++instance Loggable DdbResponse where+ toLogText (DdbResponse id2 rid) =+ "DynamoDB: request ID=" `mappend`+ fromMaybe "<none>" rid `mappend`+ ", x-amz-id-2=" `mappend`+ fromMaybe "<none>" id2++instance Monoid DdbResponse where+ mempty = DdbResponse Nothing Nothing+ mappend a b = DdbResponse (ddbrCrc a `mplus` ddbrCrc b) (ddbrMsgId a `mplus` ddbrMsgId b)+++data Region = Region {+ rUri :: B.ByteString+ , rName :: B.ByteString+ } deriving (Eq,Show,Read,Typeable)+++data DdbConfiguration qt = DdbConfiguration {+ ddbcRegion :: Region+ -- ^ The regional endpoint. Ex: 'ddbUsEast'+ , ddbcProtocol :: Protocol+ -- ^ 'HTTP' or 'HTTPS'+ , ddbcPort :: Maybe Int+ -- ^ Port override (mostly for local dev connection)+ } deriving (Show,Typeable)++instance Default (DdbConfiguration NormalQuery) where+ def = DdbConfiguration ddbUsEast1 HTTPS Nothing++instance DefaultServiceConfiguration (DdbConfiguration NormalQuery) where+ defServiceConfig = ddbHttps ddbUsEast1+ debugServiceConfig = ddbHttp ddbUsEast1+++-------------------------------------------------------------------------------+-- | DynamoDb local connection (for development)+ddbLocal :: Region+ddbLocal = Region "127.0.0.1" "local"++ddbUsEast1 :: Region+ddbUsEast1 = Region "dynamodb.us-east-1.amazonaws.com" "us-east-1"++ddbUsWest1 :: Region+ddbUsWest1 = Region "dynamodb.us-west-1.amazonaws.com" "us-west-1"++ddbUsWest2 :: Region+ddbUsWest2 = Region "dynamodb.us-west-2.amazonaws.com" "us-west-2"++ddbEuWest1 :: Region+ddbEuWest1 = Region "dynamodb.eu-west-1.amazonaws.com" "us-west-1"++ddbApNe1 :: Region+ddbApNe1 = Region "dynamodb.ap-northeast-1.amazonaws.com" "ap-northeast-1"++ddbApSe1 :: Region+ddbApSe1 = Region "dynamodb.ap-southeast-1.amazonaws.com" "ap-southeast-1"++ddbApSe2 :: Region+ddbApSe2 = Region "dynamodb.ap-southeast-2.amazonaws.com" "ap-southeast-2"++ddbSaEast1 :: Region+ddbSaEast1 = Region "dynamodb.sa-east-1.amazonaws.com" "sa-east-1"++ddbHttp :: Region -> DdbConfiguration NormalQuery+ddbHttp endpoint = DdbConfiguration endpoint HTTP Nothing++ddbHttps :: Region -> DdbConfiguration NormalQuery+ddbHttps endpoint = DdbConfiguration endpoint HTTPS Nothing+++ddbSignQuery+ :: A.ToJSON a+ => B.ByteString+ -> a+ -> DdbConfiguration qt+ -> SignatureData+ -> SignedQuery+ddbSignQuery target body di sd+ = SignedQuery {+ sqMethod = Post+ , sqProtocol = ddbcProtocol di+ , sqHost = host+ , sqPort = fromMaybe (defaultPort (ddbcProtocol di)) (ddbcPort di)+ , sqPath = "/"+ , sqQuery = []+ , sqDate = Just $ signatureTime sd+ , sqAuthorization = Just auth+ , sqContentType = Just "application/x-amz-json-1.0"+ , sqContentMd5 = Nothing+ , sqAmzHeaders = [ ("X-Amz-Target", dyApiVersion <> target)+ , ("X-Amz-Date", sigTime)+ ]+ , sqOtherHeaders = []+ , sqBody = Just $ HTTP.RequestBodyLBS bodyLBS+ , sqStringToSign = canonicalRequest+ }+ where++ Region{..} = ddbcRegion di+ host = rUri++ sigTime = fmtTime "%Y%m%dT%H%M%SZ" $ signatureTime sd++ bodyLBS = A.encode body+ bodyHash = Base16.encode $ toBytes (hashlazy bodyLBS :: Digest SHA256)++ canonicalRequest = B.concat [ "POST\n"+ , "/\n"+ , "\n" -- query string+ , "content-type:application/x-amz-json-1.0\n"+ , "host:"+ , host+ , "\n"+ , "x-amz-date:"+ , sigTime+ , "\n"+ , "x-amz-target:"+ , dyApiVersion+ , target+ , "\n"+ , "\n" -- end headers+ , "content-type;host;x-amz-date;x-amz-target\n"+ , bodyHash+ ]++ auth = authorizationV4 sd HmacSHA256 rName "dynamodb"+ "content-type;host;x-amz-date;x-amz-target"+ canonicalRequest++data AmazonError = AmazonError {+ aeType :: T.Text+ , aeMessage :: Maybe T.Text+ }++instance FromJSON AmazonError where+ parseJSON (Object v) = AmazonError+ <$> v .: "__type"+ <*> v .:? "message"+ parseJSON _ = error $ "aws: unexpected AmazonError message"+++++-------------------------------------------------------------------------------+ddbResponseConsumer :: A.FromJSON a => IORef DdbResponse -> HTTPResponseConsumer a+ddbResponseConsumer ref resp = do+ val <- HTTP.responseBody resp $$+- sinkParser A.json'+ case statusCode of+ 200 -> rSuccess val+ _ -> rError val+ where++ header = fmap T.decodeUtf8 . flip lookup (HTTP.responseHeaders resp)+ amzId = header "x-amzn-RequestId"+ amzCrc = header "x-amz-crc32"+ meta = DdbResponse amzCrc amzId+ tellMeta = liftIO $ tellMetadataRef ref meta++ rSuccess val =+ case A.fromJSON val of+ A.Success a -> return a+ A.Error err -> do+ tellMeta+ throwM $ JsonProtocolError val (T.pack err)++ rError val = do+ tellMeta+ case parseEither parseJSON val of+ Left e ->+ throwM $ JsonProtocolError val (T.pack e)++ Right err'' -> do+ let e = T.drop 1 . snd . T.breakOn "#" $ aeType err''+ errCode <- readErrCode e+ throwM $ DdbError statusCode errCode (fromMaybe "" $ aeMessage err'')++ readErrCode txt =+ let txt' = T.unpack txt+ in case readMay txt' of+ Just e -> return $ e+ Nothing -> throwM (UnknownDynamoErrCode txt)++ HTTP.Status{..} = HTTP.responseStatus resp+++-- | Conditions used by mutation operations ('PutItem', 'UpdateItem',+-- etc.). The default 'def' instance is empty (no condition).+data Conditions = Conditions CondMerge [Condition]+ deriving (Eq,Show,Read,Ord,Typeable)++instance Default Conditions where+ def = Conditions CondAnd []++++expectsJson :: Conditions -> [A.Pair]+expectsJson = conditionsJson "Expected"+++-- | JSON encoding of conditions parameter in various contexts.+conditionsJson :: T.Text -> Conditions -> [A.Pair]+conditionsJson key (Conditions op es) = b ++ a+ where+ a = if null es+ then []+ else [key .= object (map conditionJson es)]++ b = if length (take 2 es) > 1+ then ["ConditionalOperator" .= String (rendCondOp op) ]+ else []+++-------------------------------------------------------------------------------+rendCondOp :: CondMerge -> T.Text+rendCondOp CondAnd = "AND"+rendCondOp CondOr = "OR"+++-------------------------------------------------------------------------------+-- | How to merge multiple conditions.+data CondMerge = CondAnd | CondOr+ deriving (Eq,Show,Read,Ord,Typeable)+++-- | A condition used by mutation operations ('PutItem', 'UpdateItem', etc.).+data Condition = Condition {+ condAttr :: T.Text+ -- ^ Attribute to use as the basis for this conditional+ , condOp :: CondOp+ -- ^ Operation on the selected attribute+ } deriving (Eq,Show,Read,Ord,Typeable)+++-------------------------------------------------------------------------------+-- | Conditional operation to perform on a field.+data CondOp+ = DEq DValue+ | NotEq DValue+ | DLE DValue+ | DLT DValue+ | DGE DValue+ | DGT DValue+ | NotNull+ | IsNull+ | Contains DValue+ | NotContains DValue+ | Begins DValue+ | In [DValue]+ | Between DValue DValue+ deriving (Eq,Show,Read,Ord,Typeable)+++-------------------------------------------------------------------------------+getCondValues :: CondOp -> [DValue]+getCondValues c = case c of+ DEq v -> [v]+ NotEq v -> [v]+ DLE v -> [v]+ DLT v -> [v]+ DGE v -> [v]+ DGT v -> [v]+ NotNull -> []+ IsNull -> []+ Contains v -> [v]+ NotContains v -> [v]+ Begins v -> [v]+ In v -> v+ Between a b -> [a,b]+++-------------------------------------------------------------------------------+renderCondOp :: CondOp -> T.Text+renderCondOp c = case c of+ DEq{} -> "EQ"+ NotEq{} -> "NE"+ DLE{} -> "LE"+ DLT{} -> "LT"+ DGE{} -> "GE"+ DGT{} -> "GT"+ NotNull -> "NOT_NULL"+ IsNull -> "NULL"+ Contains{} -> "CONTAINS"+ NotContains{} -> "NOT_CONTAINS"+ Begins{} -> "BEGINS_WITH"+ In{} -> "IN"+ Between{} -> "BETWEEN"+++conditionJson :: Condition -> Pair+conditionJson Condition{..} = condAttr .= condOp+++instance ToJSON CondOp where+ toJSON c = object+ [ "ComparisonOperator" .= String (renderCondOp c)+ , "AttributeValueList" .= getCondValues c ]+++-------------------------------------------------------------------------------+dyApiVersion :: B.ByteString+dyApiVersion = "DynamoDB_20120810."++++-------------------------------------------------------------------------------+-- | The standard response metrics on capacity consumption.+data ConsumedCapacity = ConsumedCapacity {+ capacityUnits :: Int64+ , capacityGlobalIndex :: [(T.Text, Int64)]+ , capacityLocalIndex :: [(T.Text, Int64)]+ , capacityTableUnits :: Maybe Int64+ , capacityTable :: T.Text+ } deriving (Eq,Show,Read,Ord,Typeable)+++instance FromJSON ConsumedCapacity where+ parseJSON (Object v) = ConsumedCapacity+ <$> v .: "CapacityUnits"+ <*> (HM.toList <$> v .:? "GlobalSecondaryIndexes" .!= mempty)+ <*> (HM.toList <$> v .:? "LocalSecondaryIndexes" .!= mempty)+ <*> (v .:? "Table" >>= maybe (return Nothing) (.: "CapacityUnits"))+ <*> v .: "TableName"+ parseJSON _ = fail "ConsumedCapacity must be an Object."++++data ReturnConsumption = RCIndexes | RCTotal | RCNone+ deriving (Eq,Show,Read,Ord,Typeable)++instance ToJSON ReturnConsumption where+ toJSON RCIndexes = String "INDEXES"+ toJSON RCTotal = String "TOTAL"+ toJSON RCNone = String "NONE"++instance Default ReturnConsumption where+ def = RCNone++data ReturnItemCollectionMetrics = RICMSize | RICMNone+ deriving (Eq,Show,Read,Ord,Typeable)++instance ToJSON ReturnItemCollectionMetrics where+ toJSON RICMSize = String "SIZE"+ toJSON RICMNone = String "NONE"++instance Default ReturnItemCollectionMetrics where+ def = RICMNone+++data ItemCollectionMetrics = ItemCollectionMetrics {+ icmKey :: (T.Text, DValue)+ , icmEstimate :: [Double]+ } deriving (Eq,Show,Read,Ord,Typeable)+++instance FromJSON ItemCollectionMetrics where+ parseJSON (Object v) = ItemCollectionMetrics+ <$> (do m <- v .: "ItemCollectionKey"+ return $ head $ HM.toList m)+ <*> v .: "SizeEstimateRangeGB"+ parseJSON _ = fail "ItemCollectionMetrics must be an Object."+++-------------------------------------------------------------------------------+-- | What to return from the current update operation+data UpdateReturn+ = URNone -- ^ Return nothing+ | URAllOld -- ^ Return old values+ | URUpdatedOld -- ^ Return old values with a newer replacement+ | URAllNew -- ^ Return new values+ | URUpdatedNew -- ^ Return new values that were replacements+ deriving (Eq,Show,Read,Ord,Typeable)+++instance ToJSON UpdateReturn where+ toJSON URNone = toJSON (String "NONE")+ toJSON URAllOld = toJSON (String "ALL_OLD")+ toJSON URUpdatedOld = toJSON (String "UPDATED_OLD")+ toJSON URAllNew = toJSON (String "ALL_NEW")+ toJSON URUpdatedNew = toJSON (String "UPDATED_NEW")+++instance Default UpdateReturn where+ def = URNone++++-------------------------------------------------------------------------------+-- | What to return from a 'Query' or 'Scan' query.+data QuerySelect+ = SelectSpecific [T.Text]+ -- ^ Only return selected attributes+ | SelectCount+ -- ^ Return counts instead of attributes+ | SelectProjected+ -- ^ Return index-projected attributes+ | SelectAll+ -- ^ Default. Return everything.+ deriving (Eq,Show,Read,Ord,Typeable)+++instance Default QuerySelect where def = SelectAll++-------------------------------------------------------------------------------+querySelectJson (SelectSpecific as) =+ [ "Select" .= String "SPECIFIC_ATTRIBUTES"+ , "AttributesToGet" .= as]+querySelectJson SelectCount = ["Select" .= String "COUNT"]+querySelectJson SelectProjected = ["Select" .= String "ALL_PROJECTED_ATTRIBUTES"]+querySelectJson SelectAll = ["Select" .= String "ALL_ATTRIBUTES"]+++-------------------------------------------------------------------------------+-- | A class to help predict DynamoDb size of values, attributes and+-- entire items. The result is given in number of bytes.+class DynSize a where+ dynSize :: a -> Int++instance DynSize DValue where+ dynSize (DNum _) = 8+ dynSize (DString a) = T.length a+ dynSize (DBinary bs) = T.length . T.decodeUtf8 $ Base64.encode bs+ dynSize (DNumSet s) = 8 * S.size s+ dynSize (DStringSet s) = sum $ map (dynSize . DString) $ S.toList s+ dynSize (DBinSet s) = sum $ map (dynSize . DBinary) $ S.toList s++instance DynSize Attribute where+ dynSize (Attribute k v) = T.length k + dynSize v++instance DynSize Item where+ dynSize m = sum $ map dynSize $ attributes m++instance DynSize a => DynSize [a] where+ dynSize as = sum $ map dynSize as++instance DynSize a => DynSize (Maybe a) where+ dynSize = maybe 0 dynSize++instance (DynSize a, DynSize b) => DynSize (Either a b) where+ dynSize = either dynSize dynSize+++-------------------------------------------------------------------------------+-- | Will an attribute be considered empty by DynamoDb?+--+-- A 'PutItem' (or similar) with empty attributes will be rejection+-- with a 'ValidationException'.+nullAttr :: Attribute -> Bool+nullAttr (Attribute _ val) =+ case val of+ DString "" -> True+ DBinary "" -> True+ DNumSet s | S.null s -> True+ DStringSet s | S.null s -> True+ DBinSet s | S.null s -> True+ _ -> False++
Aws/S3/Commands.hs view
@@ -5,6 +5,7 @@ , module Aws.S3.Commands.DeleteObject , module Aws.S3.Commands.DeleteObjects , module Aws.S3.Commands.GetBucket+, module Aws.S3.Commands.GetBucketLocation , module Aws.S3.Commands.GetObject , module Aws.S3.Commands.GetService , module Aws.S3.Commands.HeadObject@@ -18,6 +19,7 @@ import Aws.S3.Commands.DeleteObject import Aws.S3.Commands.DeleteObjects import Aws.S3.Commands.GetBucket+import Aws.S3.Commands.GetBucketLocation import Aws.S3.Commands.GetObject import Aws.S3.Commands.GetService import Aws.S3.Commands.HeadObject
Aws/S3/Commands/CopyObject.hs view
@@ -6,6 +6,7 @@ import Control.Applicative import Control.Arrow (second) import Control.Monad.Trans.Resource (throwM)+import qualified Data.ByteString as B import qualified Data.CaseInsensitive as CI import Data.Maybe import qualified Data.Text as T@@ -28,13 +29,14 @@ , coIfModifiedSince :: Maybe UTCTime , coStorageClass :: Maybe StorageClass , coAcl :: Maybe CannedAcl+ , coContentType :: Maybe B.ByteString } deriving (Show) copyObject :: Bucket -> T.Text -> ObjectId -> CopyMetadataDirective -> CopyObject-copyObject bucket obj src meta = CopyObject obj bucket src meta Nothing Nothing Nothing Nothing Nothing Nothing+copyObject bucket obj src meta = CopyObject obj bucket src meta Nothing Nothing Nothing Nothing Nothing Nothing Nothing -data CopyObjectResponse +data CopyObjectResponse = CopyObjectResponse { corVersionId :: Maybe T.Text , corLastModified :: UTCTime@@ -51,7 +53,7 @@ , s3QObject = Just $ T.encodeUtf8 coObjectName , s3QSubresources = [] , s3QQuery = []- , s3QContentType = Nothing+ , s3QContentType = coContentType , s3QContentMd5 = Nothing , s3QAmzHeaders = map (second T.encodeUtf8) $ catMaybes [ Just ("x-amz-copy-source",@@ -98,7 +100,7 @@ lastMod <- forceM "Missing Last-Modified" $ el $/ elContent "LastModified" &| (parseHttpDate' . T.unpack) etag <- force "Missing ETag" $ el $/ elContent "ETag" return (lastMod, etag)- + instance Transaction CopyObject CopyObjectResponse
+ Aws/S3/Commands/GetBucketLocation.hs view
@@ -0,0 +1,56 @@+module Aws.S3.Commands.GetBucketLocation+ where++import Aws.Core+import Aws.S3.Core++import qualified Data.ByteString.Char8 as B8++import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Network.HTTP.Types as HTTP+import Text.XML.Cursor (($.//))++data GetBucketLocation+ = GetBucketLocation {+ gblBucket :: Bucket+ } deriving Show++getBucketLocation :: Bucket -> GetBucketLocation+getBucketLocation bucket+ = GetBucketLocation {+ gblBucket = bucket+ }++data GetBucketLocationResponse+ = GetBucketLocationResponse { gblrLocationConstraint :: LocationConstraint }+ deriving Show++instance SignQuery GetBucketLocation where+ type ServiceConfiguration GetBucketLocation = S3Configuration+ signQuery GetBucketLocation {..} = s3SignQuery S3Query {+ s3QMethod = Get+ , s3QBucket = Just $ T.encodeUtf8 gblBucket+ , s3QObject = Nothing+ , s3QSubresources = [("location" :: B8.ByteString, Nothing :: Maybe B8.ByteString)]+ , s3QQuery = HTTP.toQuery ([] :: [(B8.ByteString, T.Text)]) + , s3QContentType = Nothing+ , s3QContentMd5 = Nothing+ , s3QAmzHeaders = []+ , s3QOtherHeaders = []+ , s3QRequestBody = Nothing+ }++instance ResponseConsumer r GetBucketLocationResponse where+ type ResponseMetadata GetBucketLocationResponse = S3Metadata++ responseConsumer _ = s3XmlResponseConsumer parse+ where parse cursor = do+ locationConstraint <- force "Missing Location" $ cursor $.// elContent "LocationConstraint"+ return GetBucketLocationResponse { gblrLocationConstraint = normaliseLocation locationConstraint }++instance Transaction GetBucketLocation GetBucketLocationResponse++instance AsMemoryResponse GetBucketLocationResponse where+ type MemoryResponse GetBucketLocationResponse = GetBucketLocationResponse+ loadToMemory = return
Aws/S3/Commands/HeadObject.hs view
@@ -4,6 +4,7 @@ import Aws.Core import Aws.S3.Core import Control.Applicative+import Control.Monad.Trans.Resource (throwM) import Data.ByteString.Char8 ({- IsString -}) import qualified Data.ByteString.Char8 as B8 import qualified Data.Text as T@@ -24,11 +25,11 @@ data HeadObjectResponse = HeadObjectResponse {- horMetadata :: ObjectMetadata+ horMetadata :: Maybe ObjectMetadata } data HeadObjectMemoryResponse- = HeadObjectMemoryResponse ObjectMetadata+ = HeadObjectMemoryResponse (Maybe ObjectMetadata) deriving (Show) -- | ServiceConfiguration: 'S3Configuration'@@ -52,7 +53,13 @@ instance ResponseConsumer HeadObject HeadObjectResponse where type ResponseMetadata HeadObjectResponse = S3Metadata responseConsumer HeadObject{..} _ resp- = HeadObjectResponse <$> parseObjectMetadata (HTTP.responseHeaders resp)+ | status == HTTP.status200 = HeadObjectResponse . Just <$> parseObjectMetadata headers+ | status == HTTP.status404 = return $ HeadObjectResponse Nothing+ | otherwise = throwM $ HTTP.StatusCodeException status headers cookies+ where+ status = HTTP.responseStatus resp+ headers = HTTP.responseHeaders resp+ cookies = HTTP.responseCookieJar resp instance Transaction HeadObject HeadObjectResponse
Aws/S3/Commands/PutObject.hs view
@@ -33,7 +33,8 @@ #else poRequestBody :: HTTP.RequestBody (C.ResourceT IO), #endif- poMetadata :: [(T.Text,T.Text)]+ poMetadata :: [(T.Text,T.Text)],+ poAutoMakeBucket :: Bool -- ^ Internet Archive S3 nonstandard extension } #if MIN_VERSION_http_conduit(2, 0, 0)@@ -41,7 +42,7 @@ #else putObject :: Bucket -> T.Text -> HTTP.RequestBody (C.ResourceT IO) -> PutObject #endif-putObject bucket obj body = PutObject obj bucket Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing body []+putObject bucket obj body = PutObject obj bucket Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing body [] False data PutObjectResponse = PutObjectResponse {@@ -64,6 +65,7 @@ , ("x-amz-storage-class",) <$> writeStorageClass <$> poStorageClass , ("x-amz-website-redirect-location",) <$> poWebsiteRedirectLocation , ("x-amz-server-side-encryption",) <$> writeServerSideEncryption <$> poServerSideEncryption+ , if poAutoMakeBucket then Just ("x-amz-auto-make-bucket", "1") else Nothing ] ++ map( \x -> (CI.mk . T.encodeUtf8 $ T.concat ["x-amz-meta-", fst x], snd x)) poMetadata , s3QOtherHeaders = map (second T.encodeUtf8) $ catMaybes [ ("Expires",) . T.pack . show <$> poExpires
Aws/S3/Core.hs view
@@ -438,7 +438,7 @@ type LocationConstraint = T.Text -locationUsClassic, locationUsWest, locationUsWest2, locationEu, locationApSouthEast, locationApSouthEast2, locationApNorthEast :: LocationConstraint+locationUsClassic, locationUsWest, locationUsWest2, locationEu, locationApSouthEast, locationApSouthEast2, locationApNorthEast, locationSA :: LocationConstraint locationUsClassic = "" locationUsWest = "us-west-1" locationUsWest2 = "us-west-2"@@ -446,4 +446,9 @@ locationApSouthEast = "ap-southeast-1" locationApSouthEast2 = "ap-southeast-2" locationApNorthEast = "ap-northeast-1"+locationSA = "sa-east-1" +normaliseLocation :: LocationConstraint -> LocationConstraint+normaliseLocation location+ | location == "eu-west-1" = locationEu+ | otherwise = location
Aws/Sqs/Commands/Message.hs view
@@ -1,43 +1,242 @@+module Aws.Sqs.Commands.Message+(+-- * User Message Attributes+ UserMessageAttributeCustomType+, UserMessageAttributeValue(..)+, UserMessageAttributeName+, UserMessageAttribute -module Aws.Sqs.Commands.Message where+-- * Send Message+, SendMessage(..)+, SendMessageResponse(..) -import Aws.Core-import Aws.Sqs.Core-import Control.Applicative-import Control.Monad.Trans.Resource (MonadThrow)-import Data.Maybe-import Text.XML.Cursor (($/), ($//), (&/), (&|))+-- * Delete Message+, DeleteMessage(..)+, DeleteMessageResponse(..)++-- * Receive Message+, Message(..)+, ReceiveMessage(..)+, ReceiveMessageResponse(..)++-- * Change Message Visiblity+, ChangeMessageVisibility(..)+, ChangeMessageVisibilityResponse(..)+) where++import Aws.Core+import Aws.Sqs.Core+import Control.Applicative+import Control.Monad.Trans.Resource (throwM)+import Data.Maybe+import Data.Monoid+import Text.XML.Cursor (($/), ($//), (&/), (&|))+import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE-import qualified Text.XML.Cursor as Cu+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Scientific+import qualified Network.HTTP.Types as HTTP+import Text.Read (readEither)+import qualified Text.XML.Cursor as Cu -data SendMessage = SendMessage{- smMessage :: T.Text,- smQueueName :: QueueName-}deriving (Show)+-- -------------------------------------------------------------------------- --+-- User Message Attributes -data SendMessageResponse = SendMessageResponse{- smrMD5OfMessageBody :: T.Text,- smrMessageId :: MessageId-} deriving (Show)+-- | You can append a custom type label to the supported data types (String,+-- Number, and Binary) to create custom data types. This capability is similar+-- to type traits in programming languages. For example, if you have an+-- application that needs to know which type of number is being sent in the+-- message, then you could create custom types similar to the following:+-- Number.byte, Number.short, Number.int, and Number.float. Another example+-- using the binary data type is to use Binary.gif and Binary.png to+-- distinguish among different image file types in a message or batch of+-- messages. The appended data is optional and opaque to Amazon SQS, which+-- means that the appended data is not interpreted, validated, or used by+-- Amazon SQS. The Custom Type extension has the same restrictions on allowed+-- characters as the message body.+--+type UserMessageAttributeCustomType = T.Text +-- | Message Attribute Value+--+-- The user-specified message attribute value. For string data types, the value+-- attribute has the same restrictions on the content as the message body. For+-- more information, see SendMessage.+--+-- Name, type, and value must not be empty or null. In addition, the message+-- body should not be empty or null. All parts of the message attribute,+-- including name, type, and value, are included in the message size+-- restriction, which is currently 256 KB (262,144 bytes).+--+-- The supported message attribute data types are String, Number, and Binary.+-- You can also provide custom information on the type. The data type has the+-- same restrictions on the content as the message body. The data type is case+-- sensitive, and it can be up to 256 bytes long.+--+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_MessageAttributeValue.html>+--+data UserMessageAttributeValue+ = UserMessageAttributeString (Maybe UserMessageAttributeCustomType) T.Text+ -- ^ Strings are Unicode with UTF-8 binary encoding.++ | UserMessageAttributeNumber (Maybe UserMessageAttributeCustomType) Scientific+ -- ^ Numbers are positive or negative integers or floating point numbers.+ -- Numbers have sufficient range and precision to encompass most of the+ -- possible values that integers, floats, and doubles typically support. A+ -- number can have up to 38 digits of precision, and it can be between+ -- 10^-128 to 10^+126. Leading and trailing zeroes are trimmed.++ | UserMessageAttributeBinary (Maybe UserMessageAttributeCustomType) B.ByteString+ -- ^ Binary type attributes can store any binary data, for example,+ -- compressed data, encrypted data, or images.++ -- UserMessageAttributesStringList (Maybe UserMessageAttributeCustomType) [T.Text]+ -- -- ^ Not implemented. Reserved for future use.++ -- UserMessageAttributeBinaryList (Maybe UserMessageAttributeCustomType) [B.ByteString]+ -- -- ^ Not implemented. Reserved for future use.++ deriving (Show, Read, Eq, Ord)++-- | The message attribute name can contain the following characters: A-Z, a-z,+-- 0-9, underscore(_), hyphen(-), and period (.). The name must not start or+-- end with a period, and it should not have successive periods. The name is+-- case sensitive and must be unique among all attribute names for the message.+-- The name can be up to 256 characters long. The name cannot start with "AWS."+-- or "Amazon." (or any variations in casing) because these prefixes are+-- reserved for use by Amazon Web Services.+--+type UserMessageAttributeName = T.Text++-- | Message Attribute+--+-- Name, type, and value must not be empty or null. In addition, the message+-- body should not be empty or null. All parts of the message attribute,+-- including name, type, and value, are included in the message size+-- restriction, which is currently 256 KB (262,144 bytes).+--+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/SQSMessageAttributes.html#SQSMessageAttributes.DataTypes>+--+-- /NOTE/+--+-- The Amazon SQS API reference calls this /MessageAttribute/. The Haskell+-- bindings use this term for what the Amazon documentation calls just+-- /Attributes/. In order to limit backward compatibility issues we keep the+-- terminology of the Haskell bindings and call this type+-- /UserMessageAttributes/.+--+type UserMessageAttribute = (UserMessageAttributeName, UserMessageAttributeValue)++userMessageAttributesQuery :: [UserMessageAttribute] -> HTTP.Query+userMessageAttributesQuery = concat . zipWith msgAttrQuery [1 :: Int ..]+ where+ msgAttrQuery i (name, value) =+ [ ( pre <> ".Name", Just $ TE.encodeUtf8 name )+ , ( pre <> ".Value.DataType", Just typ )+ , ( pre <> ".Value." <> valueKey, Just encodedValue )+ ]+ where+ pre = "MessageAttribute." <> B.pack (show i) <> "."+ customType Nothing t = TE.encodeUtf8 t+ customType (Just c) t = TE.encodeUtf8 $ t <> "." <> c+ (typ, valueKey, encodedValue) = case value of+ UserMessageAttributeString c t ->+ (customType c "String", "StringValue", TE.encodeUtf8 t)+ UserMessageAttributeNumber c n ->+ (customType c "Number", "StringValue", B.pack $ show n)+ UserMessageAttributeBinary c b ->+ (customType c "Binary", "BinaryValue", b)++-- -------------------------------------------------------------------------- --+-- Send Message++-- | Delivers a message to the specified queue. With Amazon SQS, you now have+-- the ability to send large payload messages that are up to 256KB (262,144+-- bytes) in size. To send large payloads, you must use an AWS SDK that+-- supports SigV4 signing. To verify whether SigV4 is supported for an AWS SDK,+-- check the SDK release notes.+--+-- /IMPORTANT/+--+-- The following list shows the characters (in Unicode) allowed in your+-- message, according to the W3C XML specification. For more information, go to+-- <http://www.w3.org/TR/REC-xml/#charsets> If you send any characters not+-- included in the list, your request will be rejected.+--+-- > #x9 | #xA | #xD | [#x20 to #xD7FF] | [#xE000 to #xFFFD] | [#x10000 to #x10FFFF]+--+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_SendMessage.html>+--+data SendMessage = SendMessage+ { smMessage :: !T.Text+ -- ^ The message to send. String maximum 256 KB in size.++ , smQueueName :: !QueueName+ -- ^ The URL of the Amazon SQS queue to take action on.++ , smAttributes :: ![UserMessageAttribute]+ -- ^ Each message attribute consists of a Name, Type, and Value.++ , smDelaySeconds :: !(Maybe Int)+ -- ^ The number of seconds (0 to 900 - 15 minutes) to delay a specific+ -- message. Messages with a positive DelaySeconds value become available for+ -- processing after the delay time is finished. If you don't specify a value,+ -- the default value for the queue applies.+ }+ deriving (Show, Read, Eq, Ord)++-- | At+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_SendMessageResult.html>+-- all fields of @SendMessageResult@ are denoted as optional.+-- At+-- <http://queue.amazonaws.com/doc/2012-11-05/QueueService.wsdl>+-- all fields are specified as required.+--+-- The actual service seems to treat at least 'smrMD5OfMessageAttributes'+-- as optional.+--+data SendMessageResponse = SendMessageResponse+ { smrMD5OfMessageBody :: !T.Text+ -- ^ An MD5 digest of the non-URL-encoded message body string. This can be+ -- used to verify that Amazon SQS received the message correctly. Amazon SQS+ -- first URL decodes the message before creating the MD5 digest. For+ -- information about MD5, go to <http://www.faqs.org/rfcs/rfc1321.html>.++ , smrMessageId :: !MessageId+ -- ^ An element containing the message ID of the message sent to the queue.++ , smrMD5OfMessageAttributes :: !(Maybe T.Text)+ -- ^ An MD5 digest of the non-URL-encoded message attribute string. This can+ -- be used to verify that Amazon SQS received the message correctly. Amazon+ -- SQS first URL decodes the message before creating the MD5 digest. For+ -- information about MD5, go to <http://www.faqs.org/rfcs/rfc1321.html>.+ }+ deriving (Show, Read, Eq, Ord)+ instance ResponseConsumer r SendMessageResponse where type ResponseMetadata SendMessageResponse = SqsMetadata responseConsumer _ = sqsXmlResponseConsumer parse where- parse el = do- md5 <- force "Missing MD5 Signature" $ el $// Cu.laxElement "MD5OfMessageBody" &/ Cu.content- mid <- force "Missing Message Id" $ el $// Cu.laxElement "MessageId" &/ Cu.content- return SendMessageResponse { smrMD5OfMessageBody = md5, smrMessageId = MessageId mid }+ parse el = SendMessageResponse+ <$> force "Missing MD5 Signature"+ (el $// Cu.laxElement "MD5OfMessageBody" &/ Cu.content)+ <*> (fmap MessageId . force "Missing Message Id")+ (el $// Cu.laxElement "MessageId" &/ Cu.content)+ <*> (pure . listToMaybe)+ (el $// Cu.laxElement "MD5OfMessageAttributes" &/ Cu.content) --- | ServiceConfiguration: 'SqsConfiguration'-instance SignQuery SendMessage where- type ServiceConfiguration SendMessage = SqsConfiguration- signQuery SendMessage {..} = sqsSignQuery SqsQuery {- sqsQueueName = Just smQueueName,- sqsQuery = [("Action", Just "SendMessage"),- ("MessageBody", Just $ TE.encodeUtf8 smMessage )]}+instance SignQuery SendMessage where+ type ServiceConfiguration SendMessage = SqsConfiguration+ signQuery SendMessage{..} = sqsSignQuery SqsQuery+ { sqsQueueName = Just smQueueName+ , sqsQuery =+ [ ("Action", Just "SendMessage")+ , ("MessageBody", Just $ TE.encodeUtf8 smMessage)+ ]+ <> userMessageAttributesQuery smAttributes+ <> maybeToList (("DelaySeconds",) . Just . B.pack . show <$> smDelaySeconds)+ } instance Transaction SendMessage SendMessageResponse @@ -45,133 +244,437 @@ type MemoryResponse SendMessageResponse = SendMessageResponse loadToMemory = return -data DeleteMessage = DeleteMessage{- dmReceiptHandle :: ReceiptHandle,- dmQueueName :: QueueName -}deriving (Show)+-- -------------------------------------------------------------------------- --+-- Delete Message -data DeleteMessageResponse = DeleteMessageResponse{-} deriving (Show)+-- | Deletes the specified message from the specified queue. You specify the+-- message by using the message's receipt handle and not the message ID you+-- received when you sent the message. Even if the message is locked by another+-- reader due to the visibility timeout setting, it is still deleted from the+-- queue. If you leave a message in the queue for longer than the queue's+-- configured retention period, Amazon SQS automatically deletes it.+--+-- /NOTE/+--+-- The receipt handle is associated with a specific instance of receiving the+-- message. If you receive a message more than once, the receipt handle you get+-- each time you receive the message is different. When you request+-- DeleteMessage, if you don't provide the most recently received receipt+-- handle for the message, the request will still succeed, but the message+-- might not be deleted.+--+-- /IMPORTANT/+--+-- It is possible you will receive a message even after you have deleted it.+-- This might happen on rare occasions if one of the servers storing a copy of+-- the message is unavailable when you request to delete the message. The copy+-- remains on the server and might be returned to you again on a subsequent+-- receive request. You should create your system to be idempotent so that+-- receiving a particular message more than once is not a problem.+--+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_DeleteMessage.html>+--+data DeleteMessage = DeleteMessage+ { dmReceiptHandle :: !ReceiptHandle+ -- ^ The receipt handle associated with the message to delete.+ , dmQueueName :: !QueueName+ -- ^ The URL of the Amazon SQS queue to take action on.+ }+ deriving (Show, Read, Eq, Ord) +data DeleteMessageResponse = DeleteMessageResponse {}+ deriving (Show, Read, Eq, Ord)+ instance ResponseConsumer r DeleteMessageResponse where type ResponseMetadata DeleteMessageResponse = SqsMetadata responseConsumer _ = sqsXmlResponseConsumer parse where- parse _ = do return DeleteMessageResponse {}- --- | ServiceConfiguration: 'SqsConfiguration'-instance SignQuery DeleteMessage where - type ServiceConfiguration DeleteMessage = SqsConfiguration- signQuery DeleteMessage {..} = sqsSignQuery SqsQuery {- sqsQueueName = Just dmQueueName, - sqsQuery = [("Action", Just "DeleteMessage"), - ("ReceiptHandle", Just $ TE.encodeUtf8 $ printReceiptHandle dmReceiptHandle )]} + parse _ = return DeleteMessageResponse {} +instance SignQuery DeleteMessage where+ type ServiceConfiguration DeleteMessage = SqsConfiguration+ signQuery DeleteMessage{..} = sqsSignQuery SqsQuery+ { sqsQueueName = Just dmQueueName+ , sqsQuery =+ [ ("Action", Just "DeleteMessage")+ , ("ReceiptHandle", Just $ TE.encodeUtf8 $ printReceiptHandle dmReceiptHandle)+ ]+ }+ instance Transaction DeleteMessage DeleteMessageResponse instance AsMemoryResponse DeleteMessageResponse where type MemoryResponse DeleteMessageResponse = DeleteMessageResponse loadToMemory = return -data ReceiveMessage- = ReceiveMessage {- rmVisibilityTimeout :: Maybe Int- , rmAttributes :: [MessageAttribute]- , rmMaxNumberOfMessages :: Maybe Int- , rmQueueName :: QueueName- }- deriving (Show)+-- -------------------------------------------------------------------------- --+-- Receive Message -data Message- = Message {- mMessageId :: T.Text- , mReceiptHandle :: ReceiptHandle- , mMD5OfBody :: T.Text- , mBody :: T.Text- , mAttributes :: [(MessageAttribute,T.Text)]- }- deriving(Show)+-- | Retrieves one or more messages, with a maximum limit of 10 messages, from+-- the specified queue. Long poll support is enabled by using the+-- WaitTimeSeconds parameter. For more information, see+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-long-polling.html Amazon SQS Long Poll>+-- in the Amazon SQS Developer Guide.+--+-- Short poll is the default behavior where a weighted random set of machines+-- is sampled on a ReceiveMessage call. This means only the messages on the+-- sampled machines are returned. If the number of messages in the queue is+-- small (less than 1000), it is likely you will get fewer messages than you+-- requested per ReceiveMessage call. If the number of messages in the queue is+-- extremely small, you might not receive any messages in a particular+-- ReceiveMessage response; in which case you should repeat the request.+--+-- For each message returned, the response includes the following:+--+-- Message body+--+-- * MD5 digest of the message body. For information about MD5, go to+-- <http://www.faqs.org/rfcs/rfc1321.html>.+--+-- * Message ID you received when you sent the message to the queue.+--+-- * Receipt handle.+--+-- * Message attributes.+--+-- * MD5 digest of the message attributes.+--+-- The receipt handle is the identifier you must provide when deleting the+-- message. For more information, see Queue and Message Identifiers in the+-- Amazon SQS Developer Guide.+--+-- You can provide the VisibilityTimeout parameter in your request, which will+-- be applied to the messages that Amazon SQS returns in the response. If you+-- do not include the parameter, the overall visibility timeout for the queue+-- is used for the returned messages. For more information, see Visibility+-- Timeout in the Amazon SQS Developer Guide.+--+-- /NOTE/+--+-- Going forward, new attributes might be added. If you are writing code that+-- calls this action, we recommend that you structure your code so that it can+-- handle new attributes gracefully.+--+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_ReceiveMessage.html>+--+data ReceiveMessage = ReceiveMessage+ { rmVisibilityTimeout :: !(Maybe Int)+ -- ^ The duration (in seconds) that the received messages are hidden from+ -- subsequent retrieve requests after being retrieved by a ReceiveMessage+ -- request. -data ReceiveMessageResponse- = ReceiveMessageResponse {- rmrMessages :: [Message]- }- deriving (Show)+ , rmAttributes :: ![MessageAttribute]+ -- ^ A list of attributes that need to be returned along with each message.+ --+ -- The following lists the names and descriptions of the attributes that can+ -- be returned:+ --+ -- * All - returns all values.+ --+ -- * ApproximateFirstReceiveTimestamp - returns the time when the message was+ -- first received (epoch time in milliseconds).+ --+ -- * ApproximateReceiveCount - returns the number of times a message has been+ -- received but not deleted.+ --+ -- * SenderId - returns the AWS account number (or the IP address, if+ -- anonymous access is allowed) of the sender.+ --+ -- * SentTimestamp - returns the time when the message was sent (epoch time+ -- in milliseconds). -readMessageAttribute :: MonadThrow m => Cu.Cursor -> m (MessageAttribute,T.Text)+ , rmMaxNumberOfMessages :: !(Maybe Int)+ -- ^ The maximum number of messages to return. Amazon SQS never returns more+ -- messages than this value but may return fewer. Values can be from 1 to 10.+ -- Default is 1.+ --+ -- All of the messages are not necessarily returned.++ , rmUserMessageAttributes :: ![UserMessageAttributeName]+ -- ^ The name of the message attribute, where N is the index. The message+ -- attribute name can contain the following characters: A-Z, a-z, 0-9,+ -- underscore (_), hyphen (-), and period (.). The name must not start or end+ -- with a period, and it should not have successive periods. The name is case+ -- sensitive and must be unique among all attribute names for the message.+ -- The name can be up to 256 characters long. The name cannot start with+ -- "AWS." or "Amazon." (or any variations in casing), because these prefixes+ -- are reserved for use by Amazon Web Services.+ --+ -- When using ReceiveMessage, you can send a list of attribute names to+ -- receive, or you can return all of the attributes by specifying "All" or+ -- ".*" in your request. You can also use "foo.*" to return all message+ -- attributes starting with the "foo" prefix.++ , rmQueueName :: !QueueName+ -- ^The URL of the Amazon SQS queue to take action on.++ , rmWaitTimeSeconds :: !(Maybe Int)+ -- ^ The duration (in seconds) for which the call will wait for a message to+ -- arrive in the queue before returning. If a message is available, the call+ -- will return sooner than WaitTimeSeconds.++ }+ deriving (Show, Read, Eq, Ord)++-- | An Amazon SQS message.+--+-- In+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_Message.html>+-- all elements are denoted as optional.+-- In+-- <http://queue.amazonaws.com/doc/2012-11-05/QueueService.wsdl>+-- all elements except for the attributes are specified as required.+-- At least for the field 'mMD5OfMessageAttributes' the the service+-- is not always returning a value and therefor we make this field optional.+--+data Message = Message+ { mMessageId :: !T.Text+ -- ^ A unique identifier for the message. Message IDs are considered unique+ -- across all AWS accounts for an extended period of time.++ , mReceiptHandle :: !ReceiptHandle+ -- ^ An identifier associated with the act of receiving the message. A new+ -- receipt handle is returned every time you receive a message. When deleting+ -- a message, you provide the last received receipt handle to delete the+ -- message.++ , mMD5OfBody :: !T.Text+ -- ^ An MD5 digest of the non-URL-encoded message body string.++ , mBody :: T.Text+ -- ^ The message's contents (not URL-encoded).++ , mAttributes :: ![(MessageAttribute,T.Text)]+ -- ^ SenderId, SentTimestamp, ApproximateReceiveCount, and/or+ -- ApproximateFirstReceiveTimestamp. SentTimestamp and+ -- ApproximateFirstReceiveTimestamp are each returned as an integer+ -- representing the epoch time in milliseconds.++ , mMD5OfMessageAttributes :: !(Maybe T.Text)+ -- ^ An MD5 digest of the non-URL-encoded message attribute string. This can+ -- be used to verify that Amazon SQS received the message correctly. Amazon+ -- SQS first URL decodes the message before creating the MD5 digest. For+ -- information about MD5, go to <http://www.faqs.org/rfcs/rfc1321.html>.++ , mUserMessageAttributes :: ![UserMessageAttribute]+ -- ^ Each message attribute consists of a Name, Type, and Value.+ }+ deriving(Show, Read, Eq, Ord)++data ReceiveMessageResponse = ReceiveMessageResponse+ { rmrMessages :: ![Message]+ }+ deriving (Show, Read, Eq, Ord)++readMessageAttribute+ :: Cu.Cursor+ -> Response SqsMetadata (MessageAttribute,T.Text) readMessageAttribute cursor = do- name <- force "Missing Name" $ cursor $/ Cu.laxElement "Name" &/ Cu.content- value <- force "Missing Value" $ cursor $/ Cu.laxElement "Value" &/ Cu.content- parsedName <- parseMessageAttribute name- return (parsedName, value)+ name <- force "Missing Name" $ cursor $/ Cu.laxElement "Name" &/ Cu.content+ value <- force "Missing Value" $ cursor $/ Cu.laxElement "Value" &/ Cu.content+ parsedName <- parseMessageAttribute name+ return (parsedName, value) -readMessage :: Cu.Cursor -> [Message]+readUserMessageAttribute+ :: Cu.Cursor+ -> Response SqsMetadata UserMessageAttribute+readUserMessageAttribute cursor = (,)+ <$> force "Missing Name" (cursor $/ Cu.laxElement "Name" &/ Cu.content)+ <*> readUserMessageAttributeValue cursor++readUserMessageAttributeValue+ :: Cu.Cursor+ -> Response SqsMetadata UserMessageAttributeValue+readUserMessageAttributeValue cursor = do+ typStr <- force "Missing DataType"+ $ cursor $/ Cu.laxElement "DataType" &/ Cu.content+ case parseType typStr of+ ("String", c) -> do+ val <- force "Missing StringValue"+ $ cursor $/ Cu.laxElement "StringValue" &/ Cu.content+ return $ UserMessageAttributeString c val++ ("Number", c) -> do+ valStr <- force "Missing StringValue"+ $ cursor $/ Cu.laxElement "StringValue" &/ Cu.content+ val <- tryXml . readEither $ T.unpack valStr+ return $ UserMessageAttributeNumber c val++ ("Binary", c) -> do+ val64 <- force "Missing StringValue"+ $ cursor $/ Cu.laxElement "StringValue" &/ Cu.content+ val <- tryXml . B64.decode $ TE.encodeUtf8 val64+ return $ UserMessageAttributeBinary c val++ (x, _) -> throwM . XmlException+ $ "unkown data type for MessageAttributeValue: " <> T.unpack x+ where+ parseType s = case T.break (== '.') s of+ (a, "") -> (a, Nothing)+ (a, x) -> (a, Just (T.tail x))+ tryXml = either (throwM . XmlException) return++readMessage :: Cu.Cursor -> Response SqsMetadata Message readMessage cursor = do- mid :: T.Text <- force "Missing Message Id" $ cursor $// Cu.laxElement "MessageId" &/ Cu.content- rh <- force "Missing Reciept Handle" $ cursor $// Cu.laxElement "ReceiptHandle" &/ Cu.content- md5 <- force "Missing MD5 Signature" $ cursor $// Cu.laxElement "MD5OfBody" &/ Cu.content- body <- force "Missing Body" $ cursor $// Cu.laxElement "Body" &/ Cu.content- let attributes :: [(MessageAttribute, T.Text)] = concat $ cursor $// Cu.laxElement "Attribute" &| readMessageAttribute+ mid <- force "Missing Message Id"+ $ cursor $// Cu.laxElement "MessageId" &/ Cu.content+ rh <- force "Missing Reciept Handle"+ $ cursor $// Cu.laxElement "ReceiptHandle" &/ Cu.content+ md5 <- force "Missing MD5 Signature"+ $ cursor $// Cu.laxElement "MD5OfBody" &/ Cu.content+ body <- force "Missing Body"+ $ cursor $// Cu.laxElement "Body" &/ Cu.content+ attributes <- sequence+ $ cursor $// Cu.laxElement "Attribute" &| readMessageAttribute+ userAttributes <- sequence+ $ cursor $// Cu.laxElement "MessageAttribute" &| readUserMessageAttribute+ let md5OfMessageAttributes = listToMaybe+ $ cursor $// Cu.laxElement "MD5OfMessageAttributes" &/ Cu.content - return Message{ mMessageId = mid, mReceiptHandle = ReceiptHandle rh, mMD5OfBody = md5, mBody = body, mAttributes = attributes}+ return Message+ { mMessageId = mid+ , mReceiptHandle = ReceiptHandle rh+ , mMD5OfBody = md5+ , mBody = body+ , mAttributes = attributes+ , mMD5OfMessageAttributes = md5OfMessageAttributes+ , mUserMessageAttributes = userAttributes+ } -formatMAttributes :: [MessageAttribute] -> [(B.ByteString, Maybe B.ByteString)]-formatMAttributes attrs =- case length attrs of- 0 -> []- 1 -> [("AttributeName", Just $ B.pack $ show $ attrs !! 0)]- _ -> zipWith (\ x y -> ((B.concat ["AttributeName.", B.pack $ show $ y]), Just $ TE.encodeUtf8 $ printMessageAttribute x) ) attrs [1 :: Integer ..]+formatMAttributes :: [MessageAttribute] -> HTTP.Query+formatMAttributes attrs = case attrs of+ [attr] -> [("AttributeName", encodeAttr attr)]+ _ -> zipWith f [1 :: Int ..] attrs+ where+ f x y = ("AttributeName." <> B.pack (show x), encodeAttr y)+ encodeAttr = Just . TE.encodeUtf8 . printMessageAttribute +formatUserMessageAttributes :: [UserMessageAttributeName] -> HTTP.Query+formatUserMessageAttributes attrs = case attrs of+ [attr] -> [("MessageAttributeName", encodeAttr attr)]+ _ -> zipWith f [1 :: Int ..] attrs+ where+ f x y = ("MessageAttributeName." <> B.pack (show x), encodeAttr y)+ encodeAttr = Just . TE.encodeUtf8+ instance ResponseConsumer r ReceiveMessageResponse where type ResponseMetadata ReceiveMessageResponse = SqsMetadata responseConsumer _ = sqsXmlResponseConsumer parse where parse el = do- let messages = concat $ el $// Cu.laxElement "Message" &| readMessage- return ReceiveMessageResponse{ rmrMessages = messages }+ result <- force "Missing ReceiveMessageResult"+ $ el $// Cu.laxElement "ReceiveMessageResult"+ messages <- sequence+ $ result $// Cu.laxElement "Message" &| readMessage+ return ReceiveMessageResponse{ rmrMessages = messages } --- | ServiceConfiguration: 'SqsConfiguration' instance SignQuery ReceiveMessage where type ServiceConfiguration ReceiveMessage = SqsConfiguration- signQuery ReceiveMessage {..} = sqsSignQuery SqsQuery {- sqsQueueName = Just rmQueueName,- sqsQuery = [("Action", Just "ReceiveMessage")] ++- catMaybes[("VisibilityTimeout",) <$> case rmVisibilityTimeout of- Just x -> Just $ Just $ B.pack $ show x- Nothing -> Nothing,- ("MaxNumberOfMessages",) <$> case rmMaxNumberOfMessages of- Just x -> Just $ Just $ B.pack $ show x- Nothing -> Nothing] ++ formatMAttributes rmAttributes}+ signQuery ReceiveMessage{..} = sqsSignQuery SqsQuery+ { sqsQueueName = Just rmQueueName+ , sqsQuery = [ ("Action", Just "ReceiveMessage") ]+ <> catMaybes+ [ ("VisibilityTimeout",) <$> case rmVisibilityTimeout of+ Just x -> Just $ Just $ B.pack $ show x+ Nothing -> Nothing + , ("MaxNumberOfMessages",) <$> case rmMaxNumberOfMessages of+ Just x -> Just $ Just $ B.pack $ show x+ Nothing -> Nothing++ , ("WaitTimeSeconds",) <$> case rmWaitTimeSeconds of+ Just x -> Just $ Just $ B.pack $ show x+ Nothing -> Nothing+ ]+ <> formatMAttributes rmAttributes+ <> formatUserMessageAttributes rmUserMessageAttributes+ }+ instance Transaction ReceiveMessage ReceiveMessageResponse instance AsMemoryResponse ReceiveMessageResponse where type MemoryResponse ReceiveMessageResponse = ReceiveMessageResponse loadToMemory = return -data ChangeMessageVisibility = ChangeMessageVisibility {- cmvReceiptHandle :: ReceiptHandle,- cmvVisibilityTimeout :: Int,- cmvQueueName :: QueueName-}deriving (Show)+-- -------------------------------------------------------------------------- --+-- Change Message Visibility -data ChangeMessageVisibilityResponse = ChangeMessageVisibilityResponse{-} deriving (Show)+-- | Changes the visibility timeout of a specified message in a queue to a new+-- value. The maximum allowed timeout value you can set the value to is 12+-- hours. This means you can't extend the timeout of a message in an existing+-- queue to more than a total visibility timeout of 12 hours. (For more+-- information visibility timeout, see Visibility Timeout in the Amazon SQS+-- Developer Guide.)+--+-- For example, let's say you have a message and its default message visibility+-- timeout is 30 minutes. You could call ChangeMessageVisiblity with a value of+-- two hours and the effective timeout would be two hours and 30 minutes. When+-- that time comes near you could again extend the time out by calling+-- ChangeMessageVisiblity, but this time the maximum allowed timeout would be 9+-- hours and 30 minutes.+--+-- /NOTE/+--+-- There is a 120,000 limit for the number of inflight messages per queue.+-- Messages are inflight after they have been received from the queue by a+-- consuming component, but have not yet been deleted from the queue. If you+-- reach the 120,000 limit, you will receive an OverLimit error message from+-- Amazon SQS. To help avoid reaching the limit, you should delete the messages+-- from the queue after they have been processed. You can also increase the+-- number of queues you use to process the messages.+--+-- /IMPORTANT/+--+-- If you attempt to set the VisibilityTimeout to an amount more than the+-- maximum time left, Amazon SQS returns an error. It will not automatically+-- recalculate and increase the timeout to the maximum time remaining.+--+-- /IMPORTANT/+--+-- Unlike with a queue, when you change the visibility timeout for a specific+-- message, that timeout value is applied immediately but is not saved in+-- memory for that message. If you don't delete a message after it is received,+-- the visibility timeout for the message the next time it is received reverts+-- to the original timeout value, not the value you set with the+-- ChangeMessageVisibility action.+--+-- <http://docs.aws.amazon.com/AWSSimpleQueueService/2012-11-05/APIReference/API_ChangeMessageVisibility.html>+--+data ChangeMessageVisibility = ChangeMessageVisibility+ { cmvReceiptHandle :: !ReceiptHandle+ -- ^ The receipt handle associated with the message whose visibility timeout+ -- should be changed. This parameter is returned by the ReceiveMessage+ -- action. + , cmvVisibilityTimeout :: !Int+ -- ^ The new value (in seconds - from 0 to 43200 - maximum 12 hours) for the+ -- message's visibility timeout.++ , cmvQueueName :: !QueueName+ -- ^ The URL of the Amazon SQS queue to take action on.+ }+ deriving (Show, Read, Eq, Ord)++data ChangeMessageVisibilityResponse = ChangeMessageVisibilityResponse {}+ deriving (Show, Read, Eq, Ord)+ instance ResponseConsumer r ChangeMessageVisibilityResponse where type ResponseMetadata ChangeMessageVisibilityResponse = SqsMetadata responseConsumer _ = sqsXmlResponseConsumer parse- where - parse _ = do return ChangeMessageVisibilityResponse{}- + where+ parse _ = return ChangeMessageVisibilityResponse {}+ -- | ServiceConfiguration: 'SqsConfiguration'-instance SignQuery ChangeMessageVisibility where +instance SignQuery ChangeMessageVisibility where type ServiceConfiguration ChangeMessageVisibility = SqsConfiguration- signQuery ChangeMessageVisibility {..} = sqsSignQuery SqsQuery { - sqsQueueName = Just cmvQueueName, - sqsQuery = [("Action", Just "ChangeMessageVisibility"), - ("ReceiptHandle", Just $ TE.encodeUtf8 $ printReceiptHandle cmvReceiptHandle),- ("VisibilityTimeout", Just $ B.pack $ show cmvVisibilityTimeout)]}+ signQuery ChangeMessageVisibility {..} = sqsSignQuery SqsQuery+ { sqsQueueName = Just cmvQueueName+ , sqsQuery =+ [ ("Action", Just "ChangeMessageVisibility")+ , ("ReceiptHandle", Just . TE.encodeUtf8 $ printReceiptHandle cmvReceiptHandle)+ , ("VisibilityTimeout", Just . B.pack $ show cmvVisibilityTimeout)+ ]+ } instance Transaction ChangeMessageVisibility ChangeMessageVisibilityResponse
Aws/Sqs/Core.hs view
@@ -188,7 +188,7 @@ expandedQuery = sortBy (comparing fst) ( sqsQuery ++ [ ("AWSAccessKeyId", Just(accessKeyID signatureCredentials)), ("Expires", Just(BC.pack expiresString)), - ("SignatureMethod", Just("HmacSHA256")), ("SignatureVersion",Just("2")), ("Version",Just("2009-02-01"))+ ("SignatureMethod", Just("HmacSHA256")), ("SignatureVersion",Just("2")), ("Version",Just("2012-11-05")) ]) @@ -254,7 +254,7 @@ data QueueName = QueueName{ qName :: T.Text, qAccountNumber :: T.Text-} deriving(Show)+} deriving(Show, Read, Eq, Ord) printQueueName :: QueueName -> T.Text printQueueName queue = T.concat ["/", (qAccountNumber queue), "/", (qName queue), "/"]@@ -274,11 +274,18 @@ data MessageAttribute = MessageAll+ -- ^ all values | SenderId+ -- ^ the AWS account number (or the IP address, if anonymous access is+ -- allowed) of the sender | SentTimestamp+ -- ^ the time when the message was sent (epoch time in milliseconds) | ApproximateReceiveCount+ -- ^ the number of times a message has been received but not deleted | ApproximateFirstReceiveTimestamp- deriving(Show,Eq,Enum)+ -- ^ the time when the message was first received (epoch time in+ -- milliseconds)+ deriving(Show,Read,Eq,Ord,Enum,Bounded) data SqsPermission = PermissionAll@@ -335,8 +342,8 @@ printPermission PermissionChangeMessageVisibility = "ChangeMessageVisibility" printPermission PermissionGetQueueAttributes = "GetQueueAttributes" -newtype ReceiptHandle = ReceiptHandle T.Text deriving(Show,Eq)-newtype MessageId = MessageId T.Text deriving(Show,Eq)+newtype ReceiptHandle = ReceiptHandle T.Text deriving(Show, Read, Eq, Ord)+newtype MessageId = MessageId T.Text deriving(Show, Read, Eq, Ord) printReceiptHandle :: ReceiptHandle -> T.Text printReceiptHandle (ReceiptHandle handle) = handle
+ Examples/DynamoDb.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++-------------------------------------------------------------------------------+import Aws+import Aws.DynamoDb.Commands+import Aws.DynamoDb.Core+import Control.Concurrent+import Control.Monad+import Control.Monad.Catch+import Data.Conduit+import qualified Data.Conduit.List as C+import qualified Data.Text as T+import Network.HTTP.Conduit (withManager)+-------------------------------------------------------------------------------++createTableAndWait :: IO ()+createTableAndWait = do+ let req0 = createTable "devel-1"+ [AttributeDefinition "name" AttrString]+ (HashOnly "name")+ (ProvisionedThroughput 1 1)+ resp0 <- runCommand req0+ print resp0++ print "Waiting for table to be created"+ threadDelay (30 * 1000000)++ let req1 = DescribeTable "devel-1"+ resp1 <- runCommand req1+ print resp1++main :: IO ()+main = do+ cfg <- Aws.baseConfiguration++ createTableAndWait `catch` (\DdbError{} -> putStrLn "Table already exists")++ putStrLn "Putting an item..."++ let x = item [ attrAs text "name" "josh"+ , attrAs text "class" "not-so-awesome"]++ let req1 = (putItem "devel-1" x ) { piReturn = URAllOld+ , piRetCons = RCTotal+ , piRetMet = RICMSize+ }+++ resp1 <- runCommand req1+ print resp1++ putStrLn "Getting the item back..."++ let req2 = getItem "devel-1" (hk "name" "josh")+ resp2 <- runCommand req2+ print resp2++ print =<< runCommand+ (updateItem "devel-1" (hk "name" "josh") [au (Attribute "class" "awesome")])++ echo "Updating with false conditional."+ (print =<< runCommand+ (updateItem "devel-1" (hk "name" "josh") [au (Attribute "class" "awesomer")])+ { uiExpect = Conditions CondAnd [Condition "name" (DEq "john")] })+ `catch` (\ (e :: DdbError) -> echo ("Eating exception: " ++ show e))++ echo "Getting the item back..."+ print =<< runCommand req2+++ echo "Updating with true conditional"+ print =<< runCommand+ (updateItem "devel-1" (hk "name" "josh") [au (Attribute "class" "awesomer")])+ { uiExpect = Conditions CondAnd [Condition "name" (DEq "josh")] }++ echo "Getting the item back..."+ print =<< runCommand req2++ echo "Running a Query command..."+ print =<< runCommand (query "devel-1" (Slice (Attribute "name" "josh") Nothing))++ echo "Running a Scan command..."+ print =<< runCommand (scan "devel-1")++ echo "Filling table with several items..."+ forM_ [0..30] $ \ i -> do+ threadDelay 50000+ runCommand $ putItem "devel-1" $+ item [Attribute "name" (toValue $ T.pack ("lots-" ++ show i)), attrAs int "val" i]++ echo "Now paginating in increments of 5..."+ let q0 = (scan "devel-1") { sLimit = Just 5 }++ xs <- withManager $ \mgr -> do+ awsIteratedList cfg debugServiceConfig mgr q0 $$ C.consume+ echo ("Pagination returned " ++ show (length xs) ++ " items")+++runCommand r = do+ cfg <- Aws.baseConfiguration+ Aws.simpleAws cfg debugServiceConfig r++echo = putStrLn++
Examples/Sqs.hs view
@@ -3,12 +3,17 @@ import qualified Aws import qualified Aws.Core import qualified Aws.Sqs as Sqs+import Control.Concurrent+import Control.Error+import Control.Monad.IO.Class+import Data.Monoid+import Data.String import qualified Data.Text.IO as T import qualified Data.Text as T import qualified Data.Text.Read as TR import Control.Monad (forM_, forM, replicateM) -{-| Created by Tim Perry on September 18, 2013 +{-| Created by Tim Perry on September 18, 2013 | | All code relies on a correctly configured ~/.aws-keys and will access that account which | may incur charges for the user!@@ -26,12 +31,12 @@ main = do {- Set up AWS credentials and the default configuration. -} cfg <- Aws.baseConfiguration- let sqscfg = Sqs.sqs Aws.Core.HTTPS Sqs.sqsEndpointUsWest2 False :: Sqs.SqsConfiguration Aws.NormalQuery+ let sqscfg = Sqs.sqs Aws.Core.HTTP Sqs.sqsEndpointUsWest2 False :: Sqs.SqsConfiguration Aws.NormalQuery {- List any Queues you have already created in your SQS account -} Sqs.ListQueuesResponse qUrls <- Aws.simpleAws cfg sqscfg $ Sqs.ListQueues Nothing let origQUrlCount = length qUrls- putStrLn $ "originally had " ++ (show origQUrlCount) ++ " queue urls"+ putStrLn $ "originally had " ++ show origQUrlCount ++ " queue urls" mapM_ print qUrls {- Create a request object to create a queue and then print out the Queue URL -}@@ -41,7 +46,7 @@ T.putStrLn $ T.concat ["queue was created with Url: ", qUrl] {- Create a QueueName object, sqsQName, to hold the name of this queue for the duration -}- let awsAccountNum = (T.split (== '/') qUrl) !! 3+ let awsAccountNum = T.split (== '/') qUrl !! 3 let sqsQName = Sqs.QueueName qName awsAccountNum {- list queue attributes -- for this example we will only list the approximateNumberOfMessages in this queue. -}@@ -50,39 +55,60 @@ mapM_ (\(attName, attText) -> T.putStrLn $ T.concat [" ", Sqs.printQueueAttribute attName, " ", attText]) attPairs {- Here we add some messages to the queue -}- let messages = map (\n -> T.pack $ "msg" ++ (show n)) [1 .. 10]+ let messages = map (\n -> T.pack $ "msg" ++ show n) [1 .. 10] {- Add messages to the queue -}- forM_ messages (\mText -> do- T.putStrLn $ T.concat [" Adding: ", mText]- let sqsSendMessage = Sqs.SendMessage mText sqsQName- Aws.simpleAws cfg sqscfg sqsSendMessage)+ forM_ messages $ \mText -> do+ T.putStrLn $ " Adding: " <> mText+ let sqsSendMessage = Sqs.SendMessage mText sqsQName [] (Just 0)+ Sqs.SendMessageResponse _ mid _ <- Aws.simpleAws cfg sqscfg sqsSendMessage+ T.putStrLn $ " message id: " <> sshow mid {- Here we remove messages from the queue one at a time. -}- let receiveMessageReq = Sqs.ReceiveMessage Nothing [] (Just 1) sqsQName+ let receiveMessageReq = Sqs.ReceiveMessage Nothing [] (Just 1) [] sqsQName (Just 20) let numMessages = length messages removedMsgs <- replicateM numMessages $ do- Sqs.ReceiveMessageResponse msgs <- Aws.simpleAws cfg sqscfg receiveMessageReq+ msgs <- eitherT (const $ return []) return . retryT 2 $ do+ Sqs.ReceiveMessageResponse r <- liftIO $ Aws.simpleAws cfg sqscfg receiveMessageReq+ case r of+ [] -> left "no message received"+ _ -> right r+ putStrLn $ "number of messages received: " ++ show (length msgs) forM msgs (\msg -> do- -- here we remove a message, delete it from the queue, and then return the + -- here we remove a message, delete it from the queue, and then return the -- text sent in the body of the message- putStrLn $ "Received " ++ (show $ Sqs.mBody msg)+ putStrLn $ " Received " ++ show (Sqs.mBody msg) Aws.simpleAws cfg sqscfg $ Sqs.DeleteMessage (Sqs.mReceiptHandle msg) sqsQName return $ Sqs.mBody msg) {- Now we'll delete the queue we created at the start of this program -}- putStrLn $ "Deleting the queue: " ++ (show $ Sqs.qName sqsQName)+ putStrLn $ "Deleting the queue: " ++ show (Sqs.qName sqsQName) let dQReq = Sqs.DeleteQueue sqsQName _ <- Aws.simpleAws cfg sqscfg dQReq {- | Let's make sure the queue was actually deleted and that the same number of queues exist at when | the program ends as when it started. -}- putStrLn $ "Listing all queueus to check to see if " ++ (show $ Sqs.qName sqsQName) ++ " is gone"- Sqs.ListQueuesResponse qUrls <- Aws.simpleAws cfg sqscfg $ Sqs.ListQueues Nothing- mapM_ T.putStrLn qUrls+ eitherT T.putStrLn T.putStrLn . retryT 4 $ do+ qUrls <- liftIO $ do+ putStrLn $ "Listing all queueus to check to see if " ++ show (Sqs.qName sqsQName) ++ " is gone"+ Sqs.ListQueuesResponse qUrls_ <- Aws.simpleAws cfg sqscfg $ Sqs.ListQueues Nothing+ mapM_ T.putStrLn qUrls_+ return qUrls_ - if qUrl `elem` qUrls- then putStrLn $ " *\n *\n * Warning, '" ++ (show qName) ++ "' was not deleted\n"- ++ " * This is probably just a race condition."- else putStrLn $ " The queue '" ++ (show qName) ++ "' was correctly deleted"+ if qUrl `elem` qUrls+ then left $ " *\n *\n * Warning, '" <> sshow qName <> "' was not deleted\n"+ <> " * This is probably just a race condition."+ else right $ " The queue '" <> sshow qName <> "' was correctly deleted"++retryT :: MonadIO m => Int -> EitherT T.Text m a -> EitherT T.Text m a+retryT i f = go 1+ where+ go x+ | x >= i = fmapLT (\e -> "error after " <> sshow x <> " retries: " <> e) f+ | otherwise = f `catchT` \_ -> do+ liftIO $ threadDelay (1000000 * min 60 (2^(x-1)))+ go (succ x)++sshow :: (Show a, IsString b) => a -> b+sshow = fromString . show
README.org view
@@ -88,13 +88,20 @@ * Release Notes -** 0.9 series+** 0.10 series -- 0.9.4- - allow http-types 0.9+- 0.10+ - [EXPERIMENTAL!] DynamoDB: support for creating/updating/querying and scanning items+ - SQS: complete overhaul to support 2012-11-05 features+ - SQS: test suite+ - S3: use Maybe for 404 HEAD requests on objects instead of throwing a misleading exception+ - S3: support of poAutoMakeBucket for Internet Archive users+ - S3: implement GetBucketLocation+ - S3: add South American region+ - S3: allow specifying the Content-Type when copying objects+ - core: fix typo in NoCredentialsException accessor -- 0.9.4- - allow conduit 1.2+** 0.9 series - 0.9.3 - fix performance regression for loadCredentialsDefault@@ -252,7 +259,7 @@ | David Vollbracht | [[https://github.com/qxjit][qxjit]] | | | | | Felipe Lessa | [[https://github.com/meteficha][meteficha]] | felipe.lessa@gmail.com | currently secret | Core, S3, SES | | Nathan Howell | [[https://github.com/NathanHowell][NathanHowell]] | nhowell@alphaheavy.com | [[http://www.alphaheavy.com][Alpha Heavy Industries]] | S3 |-| Ozgun Ataman | [[https://github.com/ozataman][ozataman]] | ozgun.ataman@soostone.com | [[http://soostone.com][Soostone Inc]] | Core, S3 |+| Ozgun Ataman | [[https://github.com/ozataman][ozataman]] | ozgun.ataman@soostone.com | [[http://soostone.com][Soostone Inc]] | Core, S3, DynamoDb | | Steve Severance | [[https://github.com/sseveran][sseveran]] | sseverance@alphaheavy.com | [[http://www.alphaheavy.com][Alpha Heavy Industries]] | S3, SQS | | John Wiegley | [[https://github.com/jwiegley][jwiegley]] | johnw@fpcomplete.com | [[http://fpcomplete.com][FP Complete]] | Co-Maintainer, S3 | | Chris Dornan | [[https://github.com/cdornan][cdornan]] | chris.dornan@irisconnect.co.uk | [[http://irisconnect.co.uk][Iris Connect]] | Core |
aws.cabal view
@@ -1,5 +1,5 @@ Name: aws-Version: 0.9.5+Version: 0.10 Synopsis: Amazon Web Services (AWS) for Haskell Description: Bindings for Amazon Web Services (AWS), with the aim of supporting all AWS services. To see a high level overview of the library, see the README at <https://github.com/aristidb/aws/blob/master/README.org>. Homepage: http://github.com/aristidb/aws@@ -20,7 +20,7 @@ Source-repository this type: git location: https://github.com/aristidb/aws.git- tag: 0.9.5+ tag: 0.10 Source-repository head type: git@@ -32,84 +32,93 @@ Library Exposed-modules:- Aws,- Aws.Aws,- Aws.Core,- Aws.Ec2.InstanceMetadata,- Aws.Iam,- Aws.Iam.Commands,- Aws.Iam.Commands.CreateAccessKey,- Aws.Iam.Commands.CreateUser,- Aws.Iam.Commands.DeleteAccessKey,- Aws.Iam.Commands.DeleteUser,- Aws.Iam.Commands.DeleteUserPolicy,- Aws.Iam.Commands.GetUser,- Aws.Iam.Commands.GetUserPolicy,- Aws.Iam.Commands.ListAccessKeys,- Aws.Iam.Commands.ListUserPolicies,- Aws.Iam.Commands.ListUsers,- Aws.Iam.Commands.PutUserPolicy,- Aws.Iam.Commands.UpdateAccessKey,- Aws.Iam.Commands.UpdateUser,- Aws.Iam.Core,- Aws.Iam.Internal,- Aws.S3,- Aws.S3.Commands,- Aws.S3.Commands.CopyObject,- Aws.S3.Commands.DeleteBucket,- Aws.S3.Commands.DeleteObject,- Aws.S3.Commands.DeleteObjects,- Aws.S3.Commands.GetBucket,- Aws.S3.Commands.GetObject,- Aws.S3.Commands.GetService,- Aws.S3.Commands.HeadObject,- Aws.S3.Commands.PutBucket,- Aws.S3.Commands.PutObject,- Aws.S3.Core,- Aws.SimpleDb,- Aws.SimpleDb.Commands,- Aws.SimpleDb.Commands.Attributes,- Aws.SimpleDb.Commands.Domain,- Aws.SimpleDb.Commands.Select,- Aws.SimpleDb.Core,- Aws.Sqs,- Aws.Sqs.Commands,- Aws.Sqs.Commands.Permission,- Aws.Sqs.Commands.Message,- Aws.Sqs.Commands.Queue,- Aws.Sqs.Commands.QueueAttributes,- Aws.Sqs.Core,- Aws.Ses,- Aws.Ses.Commands,- Aws.Ses.Commands.SendRawEmail,- Aws.Ses.Commands.ListIdentities,- Aws.Ses.Commands.VerifyEmailIdentity,- Aws.Ses.Commands.VerifyDomainIdentity,- Aws.Ses.Commands.VerifyDomainDkim,- Aws.Ses.Commands.DeleteIdentity,- Aws.Ses.Commands.GetIdentityDkimAttributes,- Aws.Ses.Commands.GetIdentityNotificationAttributes,- Aws.Ses.Commands.GetIdentityVerificationAttributes,- Aws.Ses.Commands.SetIdentityNotificationTopic,- Aws.Ses.Commands.SetIdentityDkimEnabled,- Aws.Ses.Commands.SetIdentityFeedbackForwardingEnabled,- Aws.Ses.Core,- Aws.DynamoDb.Commands,- Aws.DynamoDb.Commands.Table,- Aws.DynamoDb.Core,+ Aws+ Aws.Aws+ Aws.Core+ Aws.DynamoDb+ Aws.DynamoDb.Commands+ Aws.DynamoDb.Commands.GetItem+ Aws.DynamoDb.Commands.PutItem+ Aws.DynamoDb.Commands.Query+ Aws.DynamoDb.Commands.Scan+ Aws.DynamoDb.Commands.Table+ Aws.DynamoDb.Commands.UpdateItem+ Aws.DynamoDb.Core+ Aws.Ec2.InstanceMetadata+ Aws.Iam+ Aws.Iam.Commands+ Aws.Iam.Commands.CreateAccessKey+ Aws.Iam.Commands.CreateUser+ Aws.Iam.Commands.DeleteAccessKey+ Aws.Iam.Commands.DeleteUser+ Aws.Iam.Commands.DeleteUserPolicy+ Aws.Iam.Commands.GetUser+ Aws.Iam.Commands.GetUserPolicy+ Aws.Iam.Commands.ListAccessKeys+ Aws.Iam.Commands.ListUserPolicies+ Aws.Iam.Commands.ListUsers+ Aws.Iam.Commands.PutUserPolicy+ Aws.Iam.Commands.UpdateAccessKey+ Aws.Iam.Commands.UpdateUser+ Aws.Iam.Core+ Aws.Iam.Internal Aws.Network+ Aws.S3+ Aws.S3.Commands+ Aws.S3.Commands.CopyObject+ Aws.S3.Commands.DeleteBucket+ Aws.S3.Commands.DeleteObject+ Aws.S3.Commands.DeleteObjects+ Aws.S3.Commands.GetBucket+ Aws.S3.Commands.GetBucketLocation+ Aws.S3.Commands.GetObject+ Aws.S3.Commands.GetService+ Aws.S3.Commands.HeadObject+ Aws.S3.Commands.PutBucket+ Aws.S3.Commands.PutObject+ Aws.S3.Core+ Aws.Ses+ Aws.Ses.Commands+ Aws.Ses.Commands.DeleteIdentity+ Aws.Ses.Commands.GetIdentityDkimAttributes+ Aws.Ses.Commands.GetIdentityNotificationAttributes+ Aws.Ses.Commands.GetIdentityVerificationAttributes+ Aws.Ses.Commands.ListIdentities+ Aws.Ses.Commands.SendRawEmail+ Aws.Ses.Commands.SetIdentityDkimEnabled+ Aws.Ses.Commands.SetIdentityFeedbackForwardingEnabled+ Aws.Ses.Commands.SetIdentityNotificationTopic+ Aws.Ses.Commands.VerifyDomainDkim+ Aws.Ses.Commands.VerifyDomainIdentity+ Aws.Ses.Commands.VerifyEmailIdentity+ Aws.Ses.Core+ Aws.SimpleDb+ Aws.SimpleDb.Commands+ Aws.SimpleDb.Commands.Attributes+ Aws.SimpleDb.Commands.Domain+ Aws.SimpleDb.Commands.Select+ Aws.SimpleDb.Core+ Aws.Sqs+ Aws.Sqs.Commands+ Aws.Sqs.Commands.Message+ Aws.Sqs.Commands.Permission+ Aws.Sqs.Commands.Queue+ Aws.Sqs.Commands.QueueAttributes+ Aws.Sqs.Core Build-depends: aeson >= 0.6,+ attoparsec >= 0.11 && < 0.13, base == 4.*, base16-bytestring == 0.1.*,+ base16-bytestring == 0.1.*, base64-bytestring == 1.0.*, blaze-builder >= 0.2.1.4 && < 0.4, byteable == 0.1.*, bytestring >= 0.9 && < 0.11, case-insensitive >= 0.2 && < 1.3, cereal >= 0.3 && < 0.5,- conduit >= 1.1 && < 1.3,+ conduit >= 1.1 && < 1.2, conduit-extra >= 1.1 && < 1.2, containers >= 0.4, cryptohash >= 0.11 && < 0.12,@@ -117,21 +126,24 @@ directory >= 1.0 && < 1.3, filepath >= 1.1 && < 1.4, http-conduit >= 2.1 && < 2.2,- http-types >= 0.7 && < 0.10,+ http-types >= 0.7 && < 0.9, lifted-base >= 0.1 && < 0.3, monad-control >= 0.3, mtl == 2.*, network == 2.*, old-locale == 1.*, resourcet >= 1.1 && < 1.2,+ safe >= 0.3 && < 0.4,+ scientific >= 0.3,+ tagged >= 0.7 && < 0.8, text >= 0.11, time >= 1.1.4 && < 1.5,- transformers >= 0.2.2 && < 0.5,+ transformers >= 0.2.2 && < 0.5, unordered-containers >= 0.2, utf8-string == 0.3.*, vector >= 0.10,- xml-conduit >= 1.2 && <1.3-+ xml-conduit >= 1.2 && <1.3+ if !impl(ghc >= 7.6) Build-depends: ghc-prim @@ -204,6 +216,26 @@ Default-Language: Haskell2010 +Executable DynamoDb+ Main-is: DynamoDb.hs+ Hs-source-dirs: Examples++ if !flag(Examples)+ Buildable: False+ else+ Buildable: True+ Build-depends:+ aws,+ base == 4.*,+ data-default,+ exceptions,+ http-conduit,+ text,+ conduit++ Default-Language: Haskell2010++ Executable Sqs Main-is: Sqs.hs Hs-source-dirs: Examples@@ -215,6 +247,35 @@ Build-depends: base == 4.*, aws,- text >=0.11+ errors >= 1.4,+ text >=0.11,+ transformers >= 0.3 Default-Language: Haskell2010++test-suite sqs-tests+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: tests+ main-is: Sqs/Main.hs++ other-modules:+ Utils++ build-depends:+ QuickCheck >= 2.7,+ aeson >= 0.7,+ aws,+ base == 4.*,+ bytestring >= 0.10,+ errors >= 1.4.7,+ mtl >= 2.1,+ quickcheck-instances >= 0.3,+ tagged >= 0.7,+ tasty >= 0.8,+ tasty-quickcheck >= 0.8,+ text >= 1.1,+ transformers >= 0.3++ ghc-options: -Wall -threaded+
+ tests/Sqs/Main.hs view
@@ -0,0 +1,316 @@+-- ------------------------------------------------------ --+-- Copyright © 2014 AlephCloud Systems, Inc.+-- ------------------------------------------------------ --++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE GADTs #-}++-- |+-- Module: Main+-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.+-- License: BSD3+-- Maintainer: Lars Kuhtz <lars@alephcloud.com>+-- Stability: experimental+--+-- Tests for Haskell SQS bindings+--++module Main+( main+) where++import Aws+import Aws.Core+import qualified Aws.Sqs as SQS++import Control.Arrow (second)+import Control.Error+import Control.Monad+import Control.Monad.IO.Class++import qualified Data.List as L+import Data.Monoid+import qualified Data.Text as T++import Test.Tasty+import Test.QuickCheck.Instances ()++import System.Environment+import System.Exit++import Utils++-- -------------------------------------------------------------------------- --+-- Main++main :: IO ()+main = do+ args <- getArgs+ runMain args $ map (second tail . span (/= '=')) args+ where+ runMain :: [String] -> [(String,String)] -> IO ()+ runMain args _argsMap+ | any (`elem` helpArgs) args = defaultMain tests+ | "--run-with-aws-credentials" `elem` args =+ withArgs (tastyArgs args) . defaultMain $ tests+ | otherwise = putStrLn help >> exitFailure++ helpArgs = ["--help", "-h"]+ mainArgs =+ [ "--run-with-aws-credentials"+ ]+ tastyArgs args = flip filter args $ \x -> not+ $ any (`L.isPrefixOf` x) mainArgs+++help :: String+help = L.intercalate "\n"+ [ ""+ , "NOTE"+ , ""+ , "This test suite accesses the AWS account that is associated with"+ , "the default credentials from the credential file ~/.aws-keys."+ , ""+ , "By running the tests in this test-suite costs for usage of AWS"+ , "services may incur."+ , ""+ , "In order to actually excute the tests in this test-suite you must"+ , "provide the command line options:"+ , ""+ , " --run-with-aws-credentials"+ , ""+ , "When running this test-suite through cabal you may use the following"+ , "command:"+ , ""+ , " cabal test sqs-tests --test-option=--run-with-aws-credentials"+ , ""+ ]++tests :: TestTree+tests = testGroup "SQS Tests"+ [ test_queue+ , test_message+ ]++-- -------------------------------------------------------------------------- --+-- Static Test parameters+--+-- TODO make these configurable++testProtocol :: Protocol+testProtocol = HTTP++testSqsEndpoint :: SQS.Endpoint+testSqsEndpoint = SQS.sqsEndpointUsWest2++defaultQueueName :: T.Text+defaultQueueName = "test-queue"++-- -------------------------------------------------------------------------- --+-- SQS Utils++sqsQueueName :: T.Text -> SQS.QueueName+sqsQueueName url = SQS.QueueName (sqsQueueNameText url) (sqsAccountIdText url)++sqsQueueNameText :: T.Text -> T.Text+sqsQueueNameText url = T.split (== '/') url !! 4++sqsAccountIdText :: T.Text -> T.Text+sqsAccountIdText url = T.split (== '/') url !! 3++sqsConfiguration :: SQS.SqsConfiguration qt+sqsConfiguration = SQS.SqsConfiguration+ { SQS.sqsProtocol = testProtocol+ , SQS.sqsEndpoint = testSqsEndpoint+ , SQS.sqsPort = 80+ , SQS.sqsUseUri = False+ , SQS.sqsDefaultExpiry = 180+ }++simpleSqs+ :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadIO m)+ => r+ -> m (MemoryResponse a)+simpleSqs command = do+ c <- baseConfiguration+ simpleAws c sqsConfiguration command++simpleSqsT+ :: (AsMemoryResponse a, Transaction r a, ServiceConfiguration r ~ SQS.SqsConfiguration, MonadIO m)+ => r+ -> EitherT T.Text m (MemoryResponse a)+simpleSqsT = tryT . simpleSqs++withQueueTest+ :: T.Text -- ^ Queue name+ -> (IO (T.Text, SQS.QueueName) -> TestTree) -- ^ test tree+ -> TestTree+withQueueTest queueName f = withResource createQueue deleteQueue $ \getQueueUrl ->+ f $ do+ url <- getQueueUrl+ return (url, sqsQueueName url)+ where+ createQueue = do+ SQS.CreateQueueResponse url <- simpleSqs $ SQS.CreateQueue Nothing queueName+ return url+ deleteQueue url = void $ simpleSqs (SQS.DeleteQueue (sqsQueueName url))++-- -------------------------------------------------------------------------- --+-- Queue Tests++test_queue :: TestTree+test_queue = testGroup "Queue Tests"+ [ eitherTOnceTest1 "CreateListDeleteQueue" prop_createListDeleteQueue+ ]++-- |+--+prop_createListDeleteQueue+ :: T.Text -- ^ queue name+ -> EitherT T.Text IO ()+prop_createListDeleteQueue queueName = do+ SQS.CreateQueueResponse queueUrl <- simpleSqsT $ SQS.CreateQueue Nothing tQueueName+ let queue = sqsQueueName queueUrl+ handleT (\e -> deleteQueue queue >> left e) $ do+ retryT 6 $ do+ SQS.ListQueuesResponse allQueueUrls <- simpleSqsT (SQS.ListQueues Nothing)+ unless (queueUrl `elem` allQueueUrls)+ . left $ "queue " <> sshow queueUrl <> " not listed"+ deleteQueue queue+ where+ tQueueName = testData queueName+ deleteQueue queueUrl = void $ simpleSqsT (SQS.DeleteQueue queueUrl)++-- -------------------------------------------------------------------------- --+-- Message Tests++test_message :: TestTree+test_message =+ withQueueTest defaultQueueName $ \getQueueParams -> testGroup "Queue Tests"+ [ eitherTOnceTest0 "SendReceiveDeleteMessage" $ do+ (_, queue) <- liftIO getQueueParams+ prop_sendReceiveDeleteMessage queue+ , eitherTOnceTest0 "SendReceiveDeleteMessageLongPolling" $ do+ (_, queue) <- liftIO getQueueParams+ prop_sendReceiveDeleteMessageLongPolling queue+ , eitherTOnceTest0 "SendReceiveDeleteMessageLongPolling1" $ do+ (_, queue) <- liftIO getQueueParams+ prop_sendReceiveDeleteMessageLongPolling1 queue+ ]++-- | Simple send and short-polling receive. First sends all messages+-- and receives messages thereafter one by one.+--+prop_sendReceiveDeleteMessage+ :: SQS.QueueName+ -> EitherT T.Text IO ()+prop_sendReceiveDeleteMessage queue = do++ -- a visibility timeout should be used only if either @receiveBatch == 1@+ -- or no retry is used so that all received messages are handled.+ let visTimeout = Just 60+ let delay = Just 0+ let poll = Nothing -- no consistent receive (any number of messages up to the requested number can be returned)+ let receiveBatch = 1+ let msgNum = 10++ let messages = map (\i -> "message" <> sshow i) [1 .. msgNum]++ -- send messages+ forM_ messages $ \msg -> void . simpleSqsT $ SQS.SendMessage msg queue [] delay++ recMsgs <- fmap concat . replicateM msgNum $ do+ msgs <- retryT 5 $ do+ r <- simpleSqsT $ SQS.ReceiveMessage visTimeout [] (Just receiveBatch) [] queue poll+ case r of+ SQS.ReceiveMessageResponse [] -> left "no message received"+ SQS.ReceiveMessageResponse t+ | length t <= receiveBatch -> right t+ | otherwise -> left $ "unexpected number of messages received: " <> sshow (length t)+ forM_ msgs $ \msg -> retryT 5 $+ simpleSqsT $ SQS.DeleteMessage (SQS.mReceiptHandle msg) queue+ return (map SQS.mBody msgs)++ let recv = L.sort recMsgs+ let sent = L.sort messages+ unless (sent == recv)+ $ left $ "received messages don't match send messages; sent: "+ <> sshow sent <> "; got: " <> sshow recv++-- | Checks for consistent receive: There is no message delay, so all messages+-- are available when the first receive is requested. By enabling long-polling+-- (with value 0) we force SQS to do a consistent receive.+--+prop_sendReceiveDeleteMessageLongPolling+ :: SQS.QueueName+ -> EitherT T.Text IO ()+prop_sendReceiveDeleteMessageLongPolling queue = do++ let delay = Nothing+ let visTimeout = Just 60+ let poll = Just 1 -- consistent receive (maximum available number of requested messages is returned)+ let receiveBatch = 10+ let msgNum = 40 -- this must be a multiple of 'receiveBatch'++ let messages = map (\i -> "message" <> sshow i) [1 .. msgNum]++ -- send messages+ forM_ messages $ \msg -> void . simpleSqsT $ SQS.SendMessage msg queue [] delay++ recMsgs <- fmap concat . replicateM (msgNum `div` receiveBatch) $ do+ msgs <- do+ r <- simpleSqsT $ SQS.ReceiveMessage visTimeout [] (Just receiveBatch) [] queue poll+ case r of+ SQS.ReceiveMessageResponse [] -> left "no messages received"+ SQS.ReceiveMessageResponse t+ | length t == receiveBatch -> right t+ | otherwise -> left $ "unexpected number of messages received: " <> sshow (length t)+ forM_ msgs $ \msg -> retryT 5 $+ simpleSqsT $ SQS.DeleteMessage (SQS.mReceiptHandle msg) queue+ return (map SQS.mBody msgs)++ let recv = L.sort recMsgs+ let sent = L.sort messages+ unless (sent == recv)+ $ left $ "received messages don't match send messages; sent: "+ <> sshow sent <> "; got: " <> sshow recv++-- | Checks that long polling is actually enabled. We add a delay to the messages+-- and immediately make a receive request with a polling wait time that is larger+-- than the delay. Note that even though polling forces consistent reads, messages+-- will become available with some (small) offset. Therefor we request only a single+-- message at a time.+--+prop_sendReceiveDeleteMessageLongPolling1+ :: SQS.QueueName+ -> EitherT T.Text IO ()+prop_sendReceiveDeleteMessageLongPolling1 queue = do++ let delay = Just 2+ let visTimeout = Just 60+ let poll = Just 5 -- consistent receive (maximum available number of requested messages is returned)+ let receiveBatch = 1+ let msgNum = 10 -- this must be a multiple of 'receiveBatch'++ let messages = map (\i -> "message" <> sshow i) [1 :: Int .. msgNum]++ recMsgs <- fmap concat . forM messages $ \msg -> do+ void . simpleSqsT $ SQS.SendMessage msg queue [] delay+ msgs <- do+ r <- simpleSqsT $ SQS.ReceiveMessage visTimeout [] (Just receiveBatch) [] queue poll+ case r of+ SQS.ReceiveMessageResponse [] -> left "no messages received"+ SQS.ReceiveMessageResponse t+ | length t == receiveBatch -> right t+ | otherwise -> left $ "unexpected number of messages received: " <> sshow (length t)+ forM_ msgs $ \m -> retryT 5 $+ simpleSqsT $ SQS.DeleteMessage (SQS.mReceiptHandle m) queue+ return (map SQS.mBody msgs)++ let recv = L.sort recMsgs+ let sent = L.sort messages+ unless (sent == recv)+ $ left $ "received messages don't match send messages; sent: "+ <> sshow sent <> "; got: " <> sshow recv
+ tests/Utils.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE CPP #-}++-- |+-- Module: Utils+-- Copyright: Copyright © 2014 AlephCloud Systems, Inc.+-- License: BSD3+-- Maintainer: Lars Kuhtz <lars@alephcloud.com>+-- Stability: experimental+--+-- Utils for Tests for Haskell AWS bindints+--+module Utils+(+-- * Parameters+ testDataPrefix++-- * General Utils+, sshow+, tryT+, retryT+, testData++, evalTestT+, evalTestTM+, eitherTOnceTest0+, eitherTOnceTest1+, eitherTOnceTest2++-- * Generic Tests+, test_jsonRoundtrip+, prop_jsonRoundtrip+) where++import Control.Concurrent (threadDelay)+import Control.Error+import Control.Monad+import Control.Monad.Identity+import Control.Monad.IO.Class++import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)+import Data.Monoid+import Data.Proxy+import Data.String+import qualified Data.Text as T+import Data.Typeable++import Test.QuickCheck.Property+import Test.QuickCheck.Monadic+import Test.Tasty+import Test.Tasty.QuickCheck++-- -------------------------------------------------------------------------- --+-- Static Test parameters+--++-- | This prefix is used for the IDs and names of all entities that are+-- created in the AWS account.+--+testDataPrefix :: IsString a => a+testDataPrefix = "__TEST_AWSHASKELLBINDINGS__"++-- -------------------------------------------------------------------------- --+-- General Utils++tryT :: MonadIO m => IO a -> EitherT T.Text m a+tryT = fmapLT (T.pack . show) . syncIO++testData :: (IsString a, Monoid a) => a -> a+testData a = testDataPrefix <> a++retryT :: MonadIO m => Int -> EitherT T.Text m a -> EitherT T.Text m a+retryT i f = go 1+ where+ go x+ | x >= i = fmapLT (\e -> "error after " <> sshow x <> " retries: " <> e) f+ | otherwise = f `catchT` \_ -> do+ liftIO $ threadDelay (1000000 * min 60 (2^(x-1)))+ go (succ x)++sshow :: (Show a, IsString b) => a -> b+sshow = fromString . show++evalTestTM+ :: Functor f+ => String -- ^ test name+ -> f (EitherT T.Text IO a) -- ^ test+ -> f (PropertyM IO Bool)+evalTestTM name = fmap $+ (liftIO . runEitherT) >=> \r -> case r of+ Left e ->+ fail $ "failed to run test \"" <> name <> "\": " <> show e+ Right _ -> return True++evalTestT+ :: String -- ^ test name+ -> EitherT T.Text IO a -- ^ test+ -> PropertyM IO Bool+evalTestT name = runIdentity . evalTestTM name . Identity++eitherTOnceTest0+ :: String -- ^ test name+ -> EitherT T.Text IO a -- ^ test+ -> TestTree+eitherTOnceTest0 name test = testProperty name . once . monadicIO+ $ evalTestT name test++eitherTOnceTest1+ :: (Arbitrary a, Show a)+ => String -- ^ test name+ -> (a -> EitherT T.Text IO b)+ -> TestTree+eitherTOnceTest1 name test = testProperty name . once $ monadicIO+ . evalTestTM name test++eitherTOnceTest2+ :: (Arbitrary a, Show a, Arbitrary b, Show b)+ => String -- ^ test name+ -> (a -> b -> EitherT T.Text IO c)+ -> TestTree+eitherTOnceTest2 name test = testProperty name . once $ \a b -> monadicIO+ $ (evalTestTM name $ uncurry test) (a, b)++-- -------------------------------------------------------------------------- --+-- Generic Tests++test_jsonRoundtrip+ :: forall a . (Eq a, Show a, FromJSON a, ToJSON a, Typeable a, Arbitrary a)+ => Proxy a+ -> TestTree+test_jsonRoundtrip proxy = testProperty msg (prop_jsonRoundtrip :: a -> Property)+ where+ msg = "JSON roundtrip for " <> show typ+#if MIN_VERSION_base(4,7,0)+ typ = typeRep proxy+#else+ typ = typeOf (undefined :: a)+#endif++prop_jsonRoundtrip :: forall a . (Eq a, Show a, FromJSON a, ToJSON a) => a -> Property+prop_jsonRoundtrip a = either (const $ property False) (\(b :: [a]) -> [a] === b) $+ eitherDecode $ encode [a]+