From b9f353dd8af69ea8ec14389e36e81cc786a457b3 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 1 Nov 2025 17:55:02 +1000 Subject: [PATCH 01/41] amazonka: Generate and store idempotency tokens into requests --- lib/amazonka-core/src/Amazonka/Types.hs | 12 +++++++++++- lib/amazonka/CHANGELOG.md | 3 +++ lib/amazonka/src/Amazonka/HTTP.hs | 16 ++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/amazonka-core/src/Amazonka/Types.hs b/lib/amazonka-core/src/Amazonka/Types.hs index be0b3a63b43..20501dfac03 100644 --- a/lib/amazonka-core/src/Amazonka/Types.hs +++ b/lib/amazonka-core/src/Amazonka/Types.hs @@ -677,6 +677,17 @@ class (Typeable a, Typeable (AWSResponse a)) => AWSRequest a where ClientResponse ClientBody -> m (Either Error (ClientResponse (AWSResponse a))) + -- | For some requests, botocore will designate one field of a + -- request as an "idempotency token", which the caller can manually + -- fill in to defend against deduplication. If he does not, it needs + -- to be filled in by the SDK. + -- + -- This is a 'Lens.LensLike'' and not a true 'Lens.Lens'' because if + -- the request type does not contain an idempotency token, it will + -- do nothing (violating the get/set law). + updateIdempotencyToken :: + (Functor f) => Lens.LensLike' f a (Maybe Text) + -- | An access key ID. -- -- For example: @AKIAIOSFODNN7EXAMPLE@ @@ -952,7 +963,6 @@ pattern TelAviv = Region' "il-central-1" pattern MexicoCentral :: Region pattern MexicoCentral = Region' "mx-central-1" - -- Middle East pattern Bahrain :: Region diff --git a/lib/amazonka/CHANGELOG.md b/lib/amazonka/CHANGELOG.md index 8ba53a67423..c328efc7045 100644 --- a/lib/amazonka/CHANGELOG.md +++ b/lib/amazonka/CHANGELOG.md @@ -4,6 +4,9 @@ ### Changed +- `amazonka` will now store idempotency tokens into requests that support them but have not been set by the caller. + - `amazonka-core`: Class `AWSRequest` has an new function `updateIdempotencyToken`. If a request type can contain an idempotency token, this function will allow it to be updated. + - `amazonka`: Improve error handling during periodic credential refresh in `fetchAuthInBackground`. Exceptions thrown by the refresh action are now categorized and rethrown to the parent thread as `RetrievalError`, `AuthServiceError`, or `OtherAuthError` (instead of just `RetrievalError` which was a bug). (thanks @kushagarr) diff --git a/lib/amazonka/src/Amazonka/HTTP.hs b/lib/amazonka/src/Amazonka/HTTP.hs index 8640ff0f6fa..0d81058be05 100644 --- a/lib/amazonka/src/Amazonka/HTTP.hs +++ b/lib/amazonka/src/Amazonka/HTTP.hs @@ -28,6 +28,8 @@ import Control.Monad.Trans.Resource (liftResourceT, transResourceT) import qualified Control.Retry as Retry import Data.Foldable (traverse_) import qualified Data.Time as Time +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as UUIDv4 import qualified Network.HTTP.Conduit as Client.Conduit retryRequest :: @@ -152,10 +154,16 @@ httpRequest env@Env {hooks, manager, region} cfgRq = -- (Request a). configureRequest :: (AWSRequest a, MonadIO m) => Env' withAuth -> a -> m (Request a) -configureRequest env@Env {overrides, hooks} = - liftIO - . Hooks.configuredRequest hooks env - . request overrides +configureRequest env@Env {overrides, hooks} awsRequest = do + -- If the idempotency token is not set, create a v4 UUID + -- for consistency with botocore: + -- https://github.com/boto/botocore/blob/1122d80bfeb3a52a7ae7138a9e9abdb538eae895/botocore/handlers.py#L285 + let uuidV4IfAbsent = \case + Nothing -> liftIO $ Just . UUID.toText <$> UUIDv4.nextRandom + Just token -> pure $ Just token + configuredRequest <- + request overrides <$> updateIdempotencyToken uuidV4IfAbsent awsRequest + liftIO $ Hooks.configuredRequest hooks env configuredRequest retryStream :: Request a -> Retry.RetryPolicy retryStream Request {body} = From 238c06b33c83b494273e1ed508a849c033b7d3cc Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sun, 9 Nov 2025 07:39:48 +1000 Subject: [PATCH 02/41] gen: Make instance FromJSON (RefF ()) explicit --- gen/src/Gen/Types/Service.hs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/gen/src/Gen/Types/Service.hs b/gen/src/Gen/Types/Service.hs index e878c323d4f..4b33c3d6a7c 100644 --- a/gen/src/Gen/Types/Service.hs +++ b/gen/src/Gen/Types/Service.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE TemplateHaskell #-} module Gen.Types.Service where @@ -141,17 +142,18 @@ instance HasId (RefF a) where identifier = identifier . _refShape instance FromJSON (RefF ()) where - parseJSON = Aeson.withObject "ref" $ \o -> - RefF () - <$> o .: "shape" - <*> o .:? "documentation" - <*> o .:? "location" - <*> o .:? "locationName" - <*> o .:? "resultWrapper" - <*> o .:? "queryName" - <*> o .:? "streaming" .!= False - <*> o .:? "xmlAttribute" .!= False - <*> o .:? "xmlNamespace" + parseJSON = Aeson.withObject "ref" $ \o -> do + let _refAnn = () + _refShape <- o .: "shape" + _refDocumentation <- o .:? "documentation" + _refLocation <- o .:? "location" + _refLocationName <- o .:? "locationName" + _refResultWrapper <- o .:? "resultWrapper" + _refQueryName <- o .:? "queryName" + _refStreaming <- o .:? "streaming" .!= False + _refXMLAttribute <- o .:? "xmlAttribute" .!= False + _refXMLNamespace <- o .:? "xmlNamespace" + pure RefF {..} class HasRefs f where references :: Lens.Traversal (f a) (f b) (RefF a) (RefF b) @@ -198,7 +200,7 @@ instance FromJSON Info where <*> o .:? "exception" .!= False <*> o .:? "error" -nonEmpty :: HasInfo a => a -> Bool +nonEmpty :: (HasInfo a) => a -> Bool nonEmpty = (> Just 0) . Lens.view infoMin data ListF a = ListF @@ -354,7 +356,7 @@ $(Lens.makeLenses ''Operation) operationNS :: NS -> Id -> NS operationNS ns = mappend ns . mkNS . typeId -inputName, outputName :: HasId a => Operation Identity a b -> Id +inputName, outputName :: (HasId a) => Operation Identity a b -> Id inputName = identifier . Lens.view (opInput . _Identity) outputName = identifier . Lens.view (opOutput . _Identity) @@ -372,7 +374,7 @@ instance FromJSON (Operation Maybe (RefF ()) ()) where <*> o .:? "output" <*> pure Nothing -instance ToJSON a => ToJSON (Operation Identity a b) where +instance (ToJSON a) => ToJSON (Operation Identity a b) where toJSON o = Aeson.object [ "name" .= (o ^. opName), @@ -423,7 +425,7 @@ instance FromJSON (Metadata Maybe) where instance ToJSON (Metadata Identity) where toJSON = gToJSON' camel -serviceError :: HasMetadata a f => a -> Text +serviceError :: (HasMetadata a f) => a -> Text serviceError m = case m ^. protocol of JSON -> "parseJSONError" @@ -472,7 +474,7 @@ type Ref = RefF (Shape Solved) class IsStreaming a where isStreaming :: a -> Bool - default isStreaming :: HasInfo a => a -> Bool + default isStreaming :: (HasInfo a) => a -> Bool isStreaming = Lens.view infoStreaming instance IsStreaming Info @@ -483,7 +485,7 @@ instance IsStreaming (ShapeF a) instance IsStreaming (Shape a) -instance IsStreaming a => IsStreaming (RefF a) where +instance (IsStreaming a) => IsStreaming (RefF a) where isStreaming r = _refStreaming r || isStreaming (_refAnn r) instance IsStreaming TType where From 91fae37c7cf519034cf2e935f4725b47a7ab1b73 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Mon, 10 Nov 2025 06:50:45 +1000 Subject: [PATCH 03/41] gen: Remove redundant case --- gen/src/Gen/AST/Data/Syntax.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/gen/src/Gen/AST/Data/Syntax.hs b/gen/src/Gen/AST/Data/Syntax.hs index ab61094ab1c..78c927ed35c 100644 --- a/gen/src/Gen/AST/Data/Syntax.hs +++ b/gen/src/Gen/AST/Data/Syntax.hs @@ -181,7 +181,6 @@ recordD :: (HasMetadata a Identity) => a -> Id -> [Field] -> QualConDecl recordD m n = conD . \case [] -> Exts.ConDecl () c [] - [x] -> Exts.RecDecl () c [fieldDecl (internal m) x] xs -> Exts.RecDecl () c (map (fieldDecl (internal m)) xs) where fieldDecl h f = Exts.FieldDecl () [ident (fieldAccessor f)] (h f) From ed3a590a12e6eae13c768074919991680b7f9a3c Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Mon, 10 Nov 2025 06:58:57 +1000 Subject: [PATCH 04/41] amazonka-core: typo --- lib/amazonka-core/test/Test/Amazonka/Sign/V4.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/amazonka-core/test/Test/Amazonka/Sign/V4.hs b/lib/amazonka-core/test/Test/Amazonka/Sign/V4.hs index 287228bd460..c0f00a2496c 100644 --- a/lib/amazonka-core/test/Test/Amazonka/Sign/V4.hs +++ b/lib/amazonka-core/test/Test/Amazonka/Sign/V4.hs @@ -1,5 +1,5 @@ -- | --- Module : Test.Amazonka.Sign.V$ +-- Module : Test.Amazonka.Sign.V4 -- Copyright : (c) 2013-2023 Brendan Hay -- License : Mozilla Public License, v. 2.0. -- Maintainer : Brendan Hay From 61a2c5d4da0d968761bd7a705eab7b2d59b68531 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Mon, 10 Nov 2025 07:14:07 +1000 Subject: [PATCH 05/41] gen: Rename some fields for clarity --- gen/src/Gen/AST/Data/Syntax.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gen/src/Gen/AST/Data/Syntax.hs b/gen/src/Gen/AST/Data/Syntax.hs index 78c927ed35c..38a9c8bb90f 100644 --- a/gen/src/Gen/AST/Data/Syntax.hs +++ b/gen/src/Gen/AST/Data/Syntax.hs @@ -360,14 +360,14 @@ requestD :: (Ref, [Inst]) -> (Ref, [Field]) -> Decl -requestD c m h (a, as) (b, bs) = +requestD c m h (requestRef, requestInstances) (responseRef, responseFields) = instD "Core.AWSRequest" - (identifier a) + (identifier requestRef) $ Just - [ assocD (identifier a) "AWSResponse" (typeId (identifier b)), - funArgsD "request" ["overrides"] (requestF c m h a as), - funD "response" (responseE (m ^. protocol) b bs) + [ assocD (identifier requestRef) "AWSResponse" (typeId (identifier responseRef)), + funArgsD "request" ["overrides"] (requestF c m h requestRef requestInstances), + funD "response" (responseE (m ^. protocol) responseRef responseFields) ] responseE :: Protocol -> Ref -> [Field] -> Exp From a05717e3e429edbb4d49bcb25edfe92797610766 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Mon, 10 Nov 2025 07:49:57 +1000 Subject: [PATCH 06/41] gen: Extrace AWSRequest generation into its own module --- gen/amazonka-gen.cabal | 1 + gen/src/Gen/AST/Data.hs | 3 +- gen/src/Gen/AST/Data/Instance.hs | 55 +++--- gen/src/Gen/AST/Data/Syntax.hs | 161 ------------------ gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 193 ++++++++++++++++++++++ 5 files changed, 224 insertions(+), 189 deletions(-) create mode 100644 gen/src/Gen/AST/Data/Syntax/AWSRequest.hs diff --git a/gen/amazonka-gen.cabal b/gen/amazonka-gen.cabal index f735ac54ab1..2d73e2d30f2 100644 --- a/gen/amazonka-gen.cabal +++ b/gen/amazonka-gen.cabal @@ -95,6 +95,7 @@ library Gen.AST.Data.Field Gen.AST.Data.Instance Gen.AST.Data.Syntax + Gen.AST.Data.Syntax.AWSRequest Gen.AST.Override Gen.AST.Prefix Gen.AST.Subst diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index bba4c265a76..e0d0ef4117d 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -21,6 +21,7 @@ import qualified Data.Text.Lazy as Text.Lazy import Gen.AST.Data.Field import Gen.AST.Data.Instance import Gen.AST.Data.Syntax as Syntax +import qualified Gen.AST.Data.Syntax.AWSRequest as AWSRequest import Gen.Prelude import Gen.Types import qualified Language.Haskell.Exts as Exts @@ -41,7 +42,7 @@ operationData cfg m o = do xis <- addInstances xa xs <$> requestInsts m (_opName o) h xr xs - cls <- pp Print $ requestD cfg m h (xr, xis) (yr, ys) + cls <- pp Print $ AWSRequest.instanceD cfg m h (xr, xis) (yr, ys) mpage <- pagerFields m o >>= traverse (pp Print . pagerD xn) yis' <- renderInsts p yn (responseInsts ys) diff --git a/gen/src/Gen/AST/Data/Instance.hs b/gen/src/Gen/AST/Data/Instance.hs index 95b26ef81d5..7f74294f541 100644 --- a/gen/src/Gen/AST/Data/Instance.hs +++ b/gen/src/Gen/AST/Data/Instance.hs @@ -20,6 +20,7 @@ data Inst | ToBody Field | IsHashable [Field] | IsNFData [Field] + deriving (Show) instance ToJSON Inst where toJSON = Aeson.toJSON . instToText @@ -91,7 +92,7 @@ responseInsts fs (not . null -> stream, _) = List.partition fieldStream (notLocated fs) requestInsts :: - HasMetadata a f => + (HasMetadata a f) => a -> Id -> HTTP -> @@ -142,32 +143,32 @@ requestInsts m oname h r fs = do replaceXML is | all nonEmptyXML is = pure $! filter anyXML is | otherwise = - case ( r ^? refXMLNamespace . Lens._Just . xmlUri, - r ^. refLocationName, - listToMaybe (mapMaybe findElement is) - ) of - -- 1. If there's an xmlNamespace and/or locationName on the ref, - -- it should define separate ToXML + ToElement instances - (ns, Just e, _) -> - pure $! ToElement (ns <|> m ^. xmlNamespace) (Left e) : is - -- 2. Otherwise, a single field should be found in the ToXML instance - -- and lifted to a single ToElement instance. - (_, _, Just f) -> - pure $! ToElement ns (Right f) : filter anyXML is - where - ns = - m ^. xmlNamespace - <|> f ^? fieldRef . refXMLNamespace . Lens._Just . xmlUri - - -- 3. Unknown. - (ns, e, _) -> - Left $ - "String determining root ToElement instance: " - ++ Text.unpack (memberId n) - ++ ", namespace: " - ++ show ns - ++ ", locationName: " - ++ show e + case ( r ^? refXMLNamespace . Lens._Just . xmlUri, + r ^. refLocationName, + listToMaybe (mapMaybe findElement is) + ) of + -- 1. If there's an xmlNamespace and/or locationName on the ref, + -- it should define separate ToXML + ToElement instances + (ns, Just e, _) -> + pure $! ToElement (ns <|> m ^. xmlNamespace) (Left e) : is + -- 2. Otherwise, a single field should be found in the ToXML instance + -- and lifted to a single ToElement instance. + (_, _, Just f) -> + pure $! ToElement ns (Right f) : filter anyXML is + where + ns = + m ^. xmlNamespace + <|> f ^? fieldRef . refXMLNamespace . Lens._Just . xmlUri + + -- 3. Unknown. + (ns, e, _) -> + Left $ + "String determining root ToElement instance: " + ++ Text.unpack (memberId n) + ++ ", namespace: " + ++ show ns + ++ ", locationName: " + ++ show e where nonEmptyXML = notXML True anyXML = notXML False diff --git a/gen/src/Gen/AST/Data/Syntax.hs b/gen/src/Gen/AST/Data/Syntax.hs index 38a9c8bb90f..1d455e4be34 100644 --- a/gen/src/Gen/AST/Data/Syntax.hs +++ b/gen/src/Gen/AST/Data/Syntax.hs @@ -2,7 +2,6 @@ module Gen.AST.Data.Syntax where -import qualified Control.Comonad as Comonad import qualified Control.Lens as Lens import qualified Data.Char as Char import qualified Data.Foldable as Fold @@ -352,88 +351,6 @@ notationE' withLensIso = \case TMaybe x -> var "Lens._Just" : lensIso x _other -> [] -requestD :: - (HasMetadata a Identity) => - Config -> - a -> - HTTP -> - (Ref, [Inst]) -> - (Ref, [Field]) -> - Decl -requestD c m h (requestRef, requestInstances) (responseRef, responseFields) = - instD - "Core.AWSRequest" - (identifier requestRef) - $ Just - [ assocD (identifier requestRef) "AWSResponse" (typeId (identifier responseRef)), - funArgsD "request" ["overrides"] (requestF c m h requestRef requestInstances), - funD "response" (responseE (m ^. protocol) responseRef responseFields) - ] - -responseE :: Protocol -> Ref -> [Field] -> Exp -responseE p r fs = Exts.app (responseF p r fs) bdy - where - n = r ^. Lens.to identifier - s = r ^. refAnn . Lens.to Comonad.extract - - bdy :: Exp - bdy - | null fs = var (ctorId n) - | isShared s, all fieldBody fs = lam parseAll - | otherwise = lam . ctorE n $ map parseField fs - - lam :: Exp -> Exp - lam = Exts.lamE [Exts.pvar "s", Exts.pvar "h", Exts.pvar "x"] - - parseField :: Field -> Exp - parseField x = - case fieldLocation x of - Just Headers -> parseHeadersE p x - Just Header -> parseHeadersE p x - Just StatusCode -> parseStatusE x - Just Body | body -> Exts.app pureE (var "x") - Nothing | body -> Exts.app pureE (var "x") - _ -> parseProto x - - parseProto :: Field -> Exp - parseProto f = - case p of - _ | f ^. fieldPayload -> parseOne f - JSON -> parseJSONE p pJE pJEMay pJEDef f - RestJSON -> parseJSONE p pJE pJEMay pJEDef f - APIGateway -> parseJSONE p pJE pJEMay pJEDef f - _ -> parseXMLE p f - - parseOne :: Field -> Exp - parseOne f - | fieldLit f = - if fieldIsParam f - then Exts.app (var "Prelude.pure") (var "x") - else -- Coerce is inserted here to handle newtypes such as Sensitive. - - Exts.app (var "Prelude.pure") - . Exts.paren - . Exts.app justE - . Exts.paren - . Exts.app (var "Prelude.coerce") - $ var "x" - -- This ensures anything which is set as a payload, - -- but is a primitive type is just consumed as a bytestring. - | otherwise = parseAll - - parseAll :: Exp - parseAll = - flip Exts.app (var "x") $ - if any fieldLitPayload fs - then var "Prelude.pure" - else case p of - JSON -> var "Data.eitherParseJSON" - RestJSON -> var "Data.eitherParseJSON" - APIGateway -> var "Data.eitherParseJSON" - _ -> var "Data.parseXML" - - body = any fieldStream fs - instanceD :: Protocol -> Id -> Inst -> Decl instanceD p n = \case FromXML fs -> fromXMLD p n fs @@ -594,9 +511,6 @@ funArgsD f as e = Exts.InsDecl () $ Exts.sfun (ident f) (map ident as) (unguarded e) Exts.noBinds -assocD :: Id -> Text -> Text -> InstDecl -assocD n x y = Exts.InsType () (tyapp (tycon x) (tycon (typeId n))) (tycon y) - decodeD :: Text -> Id -> Text -> ([a] -> Exp) -> [a] -> Decl decodeD c n f dec = instD1 c n . \case @@ -649,22 +563,6 @@ parseJSONE p d dm dd f n = memberName p Output f x = var "x" -parseHeadersE :: Protocol -> Field -> Exp -parseHeadersE p f - | TMap {} <- typeOf f = Exts.appFun pHMap [str n, h] - | fieldMaybe f = decodeE h pHMay n - | otherwise = decodeE h pH n - where - n = memberName p Output f - h = var "h" - -parseStatusE :: Field -> Exp -parseStatusE f - | fieldMaybe f = Exts.app pureE (Exts.app justE v) - | otherwise = Exts.app pureE v - where - v = Exts.paren $ Exts.app (var "Prelude.fromEnum") (var "s") - toXMLE :: Protocol -> Field -> Exp toXMLE p f = toGenericE p opX "Data.toXML" toXMap toXList f where @@ -785,65 +683,6 @@ inputNames, outputNames :: Protocol -> Field -> Names inputNames p f = Proto.nestedNames p Input (f ^. fieldId) (f ^. fieldRef) outputNames p f = Proto.nestedNames p Output (f ^. fieldId) (f ^. fieldRef) -requestF :: - (HasMetadata a Identity) => - Config -> - a -> - HTTP -> - Ref -> - [Inst] -> - Exp -requestF c meta h r is = - maybe e (foldr applyPlugin e) selectedPlugins - where - applyPlugin x = - -- Plugin functions are of the form :: Request a -> Request a - Exts.infixApp (var x) "Prelude.." - - selectedPlugins = - -- Lookup a specific operationPlugins key before the wildcard. - HashMap.lookup (identifier r) (c ^. operationPlugins) - <|> HashMap.lookup (mkId "*") (c ^. operationPlugins) - - e = Exts.app v (Exts.app (var "overrides") (var $ meta ^. serviceConfig)) - - v = - var - . mappend ("Request." <> methodToText m) - . fromMaybe mempty - . listToMaybe - $ mapMaybe f is - - f = \case - ToBody {} -> Just "Body" - ToJSON {} -> Just "JSON" - ToElement {} -> Just "XML" - _ - | p == Query, - m == POST -> - Just "Query" - _ - | p == EC2, - m == POST -> - Just "Query" - _ -> Nothing - - m = h ^. method - p = meta ^. protocol - --- FIXME: take method into account for responses, such as HEAD etc, particuarly --- when the body might be totally empty. -responseF :: Protocol -> RefF a -> [Field] -> Exp -responseF p r fs - | null fs = var "Response.receiveNull" - | any fieldStream fs = var "Response.receiveBody" - | any fieldLitPayload fs = var "Response.receiveBytes" - | Just x <- r ^. refResultWrapper = Exts.app (var (suf <> "Wrapper")) (str x) - | not $ any fieldBody fs = var "Response.receiveEmpty" - | otherwise = var suf - where - suf = "Response.receive" <> Proto.suffix p - waiterS :: Id -> Waiter a -> Decl waiterS n w = Exts.TypeSig () [ident c] $ tyapp (tycon "Core.Wait") (tycon k) where diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs new file mode 100644 index 00000000000..157209b9a02 --- /dev/null +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -0,0 +1,193 @@ +module Gen.AST.Data.Syntax.AWSRequest where + +import qualified Control.Comonad as Comonad +import qualified Control.Lens as Lens +import qualified Data.HashMap.Strict as HashMap +import Gen.AST.Data.Field (Field, fieldBody, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) +import Gen.AST.Data.Instance (Inst (..)) +import Gen.AST.Data.Syntax + ( ctorE, + decodeE, + funArgsD, + funD, + instD, + justE, + memberName, + pH, + pHMap, + pHMay, + pJE, + pJEDef, + pJEMay, + parseJSONE, + parseXMLE, + pureE, + str, + tyapp, + tycon, + var, + ) +import Gen.Prelude +import qualified Gen.Protocol as Proto +import Gen.Types +import qualified Language.Haskell.Exts as Exts + +instanceD :: + (HasMetadata a Identity) => + Config -> + a -> + HTTP -> + (Ref, [Inst]) -> + (Ref, [Field]) -> + Exts.Decl () +instanceD c m h (requestRef, requestInstances) (responseRef, responseFields) = + instD + "Core.AWSRequest" + (identifier requestRef) + $ Just + [ assocD (identifier requestRef) "AWSResponse" (typeId (identifier responseRef)), + funArgsD "request" ["overrides"] (requestF c m h requestRef requestInstances), + funD "response" (responseE (m ^. protocol) responseRef responseFields) + ] + +assocD :: Id -> Text -> Text -> Exts.InstDecl () +assocD n x y = Exts.InsType () (tyapp (tycon x) (tycon (typeId n))) (tycon y) + +requestF :: + (HasMetadata a Identity) => + Config -> + a -> + HTTP -> + Ref -> + [Inst] -> + Exts.Exp () +requestF c meta h r is = + maybe e (foldr applyPlugin e) selectedPlugins + where + applyPlugin x = + -- Plugin functions are of the form :: Request a -> Request a + Exts.infixApp (var x) "Prelude.." + + selectedPlugins = + -- Lookup a specific operationPlugins key before the wildcard. + HashMap.lookup (identifier r) (c ^. operationPlugins) + <|> HashMap.lookup (mkId "*") (c ^. operationPlugins) + + e = Exts.app v (Exts.app (var "overrides") (var $ meta ^. serviceConfig)) + + v = + var + . mappend ("Request." <> methodToText m) + . fromMaybe mempty + . listToMaybe + $ mapMaybe f is + + f = \case + ToBody {} -> Just "Body" + ToJSON {} -> Just "JSON" + ToElement {} -> Just "XML" + _ + | p == Query, + m == POST -> + Just "Query" + _ + | p == EC2, + m == POST -> + Just "Query" + _ -> Nothing + + m = h ^. method + p = meta ^. protocol + +responseE :: Protocol -> Ref -> [Field] -> Exts.Exp () +responseE p r fs = Exts.app (responseF p r fs) bdy + where + n = r ^. Lens.to identifier + s = r ^. refAnn . Lens.to Comonad.extract + + bdy :: Exts.Exp () + bdy + | null fs = var (ctorId n) + | isShared s, all fieldBody fs = lam parseAll + | otherwise = lam . ctorE n $ map parseField fs + + lam :: Exts.Exp () -> Exts.Exp () + lam = Exts.lamE [Exts.pvar "s", Exts.pvar "h", Exts.pvar "x"] + + parseField :: Field -> Exts.Exp () + parseField x = + case fieldLocation x of + Just Headers -> parseHeadersE p x + Just Header -> parseHeadersE p x + Just StatusCode -> parseStatusE x + Just Body | body -> Exts.app pureE (var "x") + Nothing | body -> Exts.app pureE (var "x") + _ -> parseProto x + + parseProto :: Field -> Exts.Exp () + parseProto f = + case p of + _ | f ^. fieldPayload -> parseOne f + JSON -> parseJSONE p pJE pJEMay pJEDef f + RestJSON -> parseJSONE p pJE pJEMay pJEDef f + APIGateway -> parseJSONE p pJE pJEMay pJEDef f + _ -> parseXMLE p f + + parseOne :: Field -> Exts.Exp () + parseOne f + | fieldLit f = + if fieldIsParam f + then Exts.app (var "Prelude.pure") (var "x") + else -- Coerce is inserted here to handle newtypes such as Sensitive. + + Exts.app (var "Prelude.pure") + . Exts.paren + . Exts.app justE + . Exts.paren + . Exts.app (var "Prelude.coerce") + $ var "x" + -- This ensures anything which is set as a payload, + -- but is a primitive type is just consumed as a bytestring. + | otherwise = parseAll + + parseAll :: Exts.Exp () + parseAll = + flip Exts.app (var "x") $ + if any fieldLitPayload fs + then var "Prelude.pure" + else case p of + JSON -> var "Data.eitherParseJSON" + RestJSON -> var "Data.eitherParseJSON" + APIGateway -> var "Data.eitherParseJSON" + _ -> var "Data.parseXML" + + body = any fieldStream fs + +-- FIXME: take method into account for responses, such as HEAD etc, particuarly +-- when the body might be totally empty. +responseF :: Protocol -> RefF a -> [Field] -> Exts.Exp () +responseF p r fs + | null fs = var "Response.receiveNull" + | any fieldStream fs = var "Response.receiveBody" + | any fieldLitPayload fs = var "Response.receiveBytes" + | Just x <- r ^. refResultWrapper = Exts.app (var (suf <> "Wrapper")) (str x) + | not $ any fieldBody fs = var "Response.receiveEmpty" + | otherwise = var suf + where + suf = "Response.receive" <> Proto.suffix p + +parseHeadersE :: Protocol -> Field -> Exts.Exp () +parseHeadersE p f + | TMap {} <- typeOf f = Exts.appFun pHMap [str n, h] + | fieldMaybe f = decodeE h pHMay n + | otherwise = decodeE h pH n + where + n = memberName p Output f + h = var "h" + +parseStatusE :: Field -> Exts.Exp () +parseStatusE f + | fieldMaybe f = Exts.app pureE (Exts.app justE v) + | otherwise = Exts.app pureE v + where + v = Exts.paren $ Exts.app (var "Prelude.fromEnum") (var "s") From 29152fe315158b7d77942b10dda43c094bb97305 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Mon, 10 Nov 2025 07:57:15 +1000 Subject: [PATCH 07/41] gen(AWSRequest): be explicit that we consume `Metadata f` --- gen/src/Gen/AST/Data.hs | 2 +- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index e0d0ef4117d..3ad66a28cdb 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -42,7 +42,7 @@ operationData cfg m o = do xis <- addInstances xa xs <$> requestInsts m (_opName o) h xr xs - cls <- pp Print $ AWSRequest.instanceD cfg m h (xr, xis) (yr, ys) + cls <- pp Print $ AWSRequest.instanceD cfg (m ^. metadata) h (xr, xis) (yr, ys) mpage <- pagerFields m o >>= traverse (pp Print . pagerD xn) yis' <- renderInsts p yn (responseInsts ys) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 157209b9a02..326a8c74244 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -33,9 +33,8 @@ import Gen.Types import qualified Language.Haskell.Exts as Exts instanceD :: - (HasMetadata a Identity) => Config -> - a -> + Metadata f -> HTTP -> (Ref, [Inst]) -> (Ref, [Field]) -> @@ -54,9 +53,8 @@ assocD :: Id -> Text -> Text -> Exts.InstDecl () assocD n x y = Exts.InsType () (tyapp (tycon x) (tycon (typeId n))) (tycon y) requestF :: - (HasMetadata a Identity) => Config -> - a -> + Metadata f -> HTTP -> Ref -> [Inst] -> From 77b8880f85f2f202d265417d6fdaa64bcae57771 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Mon, 10 Nov 2025 09:50:22 +1000 Subject: [PATCH 08/41] gen(Metadata): Use ApplicativeDo idiom for FromJSON instance --- gen/src/Gen/Types/Service.hs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/gen/src/Gen/Types/Service.hs b/gen/src/Gen/Types/Service.hs index 4b33c3d6a7c..7dfd215aea1 100644 --- a/gen/src/Gen/Types/Service.hs +++ b/gen/src/Gen/Types/Service.hs @@ -407,20 +407,20 @@ deriving instance Show (Metadata Identity) $(Lens.makeClassy ''Metadata) instance FromJSON (Metadata Maybe) where - parseJSON = Aeson.withObject "meta" $ \o -> - Metadata - <$> o .: "protocol" - <*> o .: "serviceAbbreviation" - <*> (o .: "serviceAbbreviation" <&> renameServiceFunction) - <*> (o .: "serviceFullName" <&> renameService) - <*> (o .: "signingName" <|> o .: "endpointPrefix") - <*> o .: "apiVersion" - <*> o .: "signatureVersion" - <*> o .: "endpointPrefix" - <*> o .:? "checksumFormat" - <*> o .:? "xmlNamespace" - <*> o .:? "jsonVersion" - <*> o .:? "targetPrefix" + parseJSON = Aeson.withObject "meta" $ \o -> do + _protocol <- o .: "protocol" + _serviceAbbrev <- o .: "serviceAbbreviation" + _serviceConfig <- o .: "serviceAbbreviation" <&> renameServiceFunction + _serviceFullName <- o .: "serviceFullName" <&> renameService + _signingName <- o .: "signingName" <|> o .: "endpointPrefix" + _apiVersion <- o .: "apiVersion" + _signatureVersion <- o .: "signatureVersion" + _endpointPrefix <- o .: "endpointPrefix" + _checksumFormat <- o .:? "checksumFormat" + _xmlNamespace <- o .:? "xmlNamespace" + _jsonVersion <- o .:? "jsonVersion" + _targetPrefix <- o .:? "targetPrefix" + pure Metadata {..} instance ToJSON (Metadata Identity) where toJSON = gToJSON' camel From b1c27b63406f513aee5563f2b1ad34f9c4560a0e Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Tue, 11 Nov 2025 08:34:49 +1000 Subject: [PATCH 09/41] gen: Extract request function generation --- gen/src/Gen/AST/Data.hs | 34 +++++++++- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 76 ++++++++--------------- 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 3ad66a28cdb..64f61e0543f 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -23,7 +23,7 @@ import Gen.AST.Data.Instance import Gen.AST.Data.Syntax as Syntax import qualified Gen.AST.Data.Syntax.AWSRequest as AWSRequest import Gen.Prelude -import Gen.Types +import Gen.Types hiding (method) import qualified Language.Haskell.Exts as Exts import Language.Haskell.Exts.Pretty (Pretty) @@ -42,7 +42,37 @@ operationData cfg m o = do xis <- addInstances xa xs <$> requestInsts m (_opName o) h xr xs - cls <- pp Print $ AWSRequest.instanceD cfg (m ^. metadata) h (xr, xis) (yr, ys) + let requestFunction = method <> format + where + method = methodToText $ _method h + format = + case (mapMaybe fromInstance xis, _method h, m ^. protocol) of + (f : _, _, _) -> f + ([], POST, Query) -> "Query" + ([], POST, EC2) -> "Query" + _ -> "" + fromInstance = \case + ToBody {} -> Just "Body" + ToJSON {} -> Just "JSON" + ToElement {} -> Just "XML" + _ -> Nothing + + cls <- + pp Print $ + AWSRequest.instanceD + AWSRequest.Config + { -- Lookup a specific operationPlugins key before checking + -- for the wildcard. + operationPlugins = + fromMaybe [] $ + (cfg ^. operationPlugins . Lens.at (identifier xr)) + <|> (cfg ^. operationPlugins . Lens.at (mkId "*")), + requestFunction, + serviceConfig = m ^. serviceConfig + } + (m ^. metadata) + xr + (yr, ys) mpage <- pagerFields m o >>= traverse (pp Print . pagerD xn) yis' <- renderInsts p yn (responseInsts ys) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 326a8c74244..da2b0bc4940 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -2,9 +2,7 @@ module Gen.AST.Data.Syntax.AWSRequest where import qualified Control.Comonad as Comonad import qualified Control.Lens as Lens -import qualified Data.HashMap.Strict as HashMap import Gen.AST.Data.Field (Field, fieldBody, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) -import Gen.AST.Data.Instance (Inst (..)) import Gen.AST.Data.Syntax ( ctorE, decodeE, @@ -29,73 +27,51 @@ import Gen.AST.Data.Syntax ) import Gen.Prelude import qualified Gen.Protocol as Proto -import Gen.Types +import Gen.Types hiding (Config, operationPlugins, serviceConfig) import qualified Language.Haskell.Exts as Exts +data Config = Config + { -- | List of function names to apply to computed request the + -- implementation of the @request@ function. + operationPlugins :: [Text], + -- | Name of the request function to call (@putBody@, @postQuery@, ...). + -- from the @"Request"@ module. + requestFunction :: Text, + -- | Name of the service config value to use by default. As of + -- 2025-11, always @"defaultService"@; the parser for 'Metadata' + -- would override the service abbrev in all cases. + serviceConfig :: Text + } + instanceD :: Config -> Metadata f -> - HTTP -> - (Ref, [Inst]) -> + Ref -> (Ref, [Field]) -> Exts.Decl () -instanceD c m h (requestRef, requestInstances) (responseRef, responseFields) = +instanceD c m requestRef (responseRef, responseFields) = instD "Core.AWSRequest" (identifier requestRef) $ Just [ assocD (identifier requestRef) "AWSResponse" (typeId (identifier responseRef)), - funArgsD "request" ["overrides"] (requestF c m h requestRef requestInstances), + funArgsD "request" ["overrides"] (requestF c), funD "response" (responseE (m ^. protocol) responseRef responseFields) ] assocD :: Id -> Text -> Text -> Exts.InstDecl () assocD n x y = Exts.InsType () (tyapp (tycon x) (tycon (typeId n))) (tycon y) -requestF :: - Config -> - Metadata f -> - HTTP -> - Ref -> - [Inst] -> - Exts.Exp () -requestF c meta h r is = - maybe e (foldr applyPlugin e) selectedPlugins +requestF :: Config -> Exts.Exp () +requestF Config {..} = foldr applyPlugin e operationPlugins where - applyPlugin x = - -- Plugin functions are of the form :: Request a -> Request a - Exts.infixApp (var x) "Prelude.." - - selectedPlugins = - -- Lookup a specific operationPlugins key before the wildcard. - HashMap.lookup (identifier r) (c ^. operationPlugins) - <|> HashMap.lookup (mkId "*") (c ^. operationPlugins) - - e = Exts.app v (Exts.app (var "overrides") (var $ meta ^. serviceConfig)) - - v = - var - . mappend ("Request." <> methodToText m) - . fromMaybe mempty - . listToMaybe - $ mapMaybe f is - - f = \case - ToBody {} -> Just "Body" - ToJSON {} -> Just "JSON" - ToElement {} -> Just "XML" - _ - | p == Query, - m == POST -> - Just "Query" - _ - | p == EC2, - m == POST -> - Just "Query" - _ -> Nothing - - m = h ^. method - p = meta ^. protocol + -- Plugin functions are of the form :: Request a -> Request a + applyPlugin x = Exts.infixApp (var x) "Prelude.." + + e = + Exts.app + (var $ "Request." <> requestFunction) + (Exts.app (var "overrides") (var serviceConfig)) responseE :: Protocol -> Ref -> [Field] -> Exts.Exp () responseE p r fs = Exts.app (responseF p r fs) bdy From 43f7bc64b9c82e1e3170d9e42883022242ad410b Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Tue, 11 Nov 2025 08:44:17 +1000 Subject: [PATCH 10/41] gen (AWSRequest): Push all of the request declaration into requestF --- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index da2b0bc4940..40a48d02353 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -55,15 +55,16 @@ instanceD c m requestRef (responseRef, responseFields) = (identifier requestRef) $ Just [ assocD (identifier requestRef) "AWSResponse" (typeId (identifier responseRef)), - funArgsD "request" ["overrides"] (requestF c), + requestD c, funD "response" (responseE (m ^. protocol) responseRef responseFields) ] assocD :: Id -> Text -> Text -> Exts.InstDecl () assocD n x y = Exts.InsType () (tyapp (tycon x) (tycon (typeId n))) (tycon y) -requestF :: Config -> Exts.Exp () -requestF Config {..} = foldr applyPlugin e operationPlugins +requestD :: Config -> Exts.InstDecl () +requestD Config {..} = + funArgsD "request" ["overrides"] $ foldr applyPlugin e operationPlugins where -- Plugin functions are of the form :: Request a -> Request a applyPlugin x = Exts.infixApp (var x) "Prelude.." From 32b70dda6ce6020ee7acb9f271358e792d40a5af Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 12 Nov 2025 07:37:09 +1000 Subject: [PATCH 11/41] gen(AWSRequest): Start converting to depend on responseType (Id) --- gen/src/Gen/AST/Data.hs | 7 ++--- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 31 ++++++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 64f61e0543f..3eb05d1dcdc 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -61,17 +61,18 @@ operationData cfg m o = do pp Print $ AWSRequest.instanceD AWSRequest.Config - { -- Lookup a specific operationPlugins key before checking + { requestType = identifier xr, + requestFunction, + responseType = identifier yr, + -- Lookup a specific operationPlugins key before checking -- for the wildcard. operationPlugins = fromMaybe [] $ (cfg ^. operationPlugins . Lens.at (identifier xr)) <|> (cfg ^. operationPlugins . Lens.at (mkId "*")), - requestFunction, serviceConfig = m ^. serviceConfig } (m ^. metadata) - xr (yr, ys) mpage <- pagerFields m o >>= traverse (pp Print . pagerD xn) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 40a48d02353..d6919fc0a20 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -31,12 +31,18 @@ import Gen.Types hiding (Config, operationPlugins, serviceConfig) import qualified Language.Haskell.Exts as Exts data Config = Config - { -- | List of function names to apply to computed request the - -- implementation of the @request@ function. - operationPlugins :: [Text], + { -- | Name of the data type we generate to represent the input + -- shape from botocore. + requestType :: Id, -- | Name of the request function to call (@putBody@, @postQuery@, ...). -- from the @"Request"@ module. requestFunction :: Text, + -- | List of function names to apply to the computed request in + -- the implementation of the @request@ function. + operationPlugins :: [Text], + -- | Name of the data type we generate for the botocore output + -- shape that corresponds to 'requestType'. + responseType :: Id, -- | Name of the service config value to use by default. As of -- 2025-11, always @"defaultService"@; the parser for 'Metadata' -- would override the service abbrev in all cases. @@ -46,21 +52,22 @@ data Config = Config instanceD :: Config -> Metadata f -> - Ref -> (Ref, [Field]) -> Exts.Decl () -instanceD c m requestRef (responseRef, responseFields) = - instD - "Core.AWSRequest" - (identifier requestRef) - $ Just - [ assocD (identifier requestRef) "AWSResponse" (typeId (identifier responseRef)), +instanceD c@Config {requestType} m (responseRef, responseFields) = + instD "Core.AWSRequest" requestType $ + Just + [ awsResponseD c, requestD c, funD "response" (responseE (m ^. protocol) responseRef responseFields) ] -assocD :: Id -> Text -> Text -> Exts.InstDecl () -assocD n x y = Exts.InsType () (tyapp (tycon x) (tycon (typeId n))) (tycon y) +awsResponseD :: Config -> Exts.InstDecl () +awsResponseD Config {..} = + Exts.InsType + () + (tycon "AWSResponse" `tyapp` tycon (typeId requestType)) + (tycon (typeId responseType)) requestD :: Config -> Exts.InstDecl () requestD Config {..} = From 9707eb0b2a01d4250078a8cf8d5207f4e46af827 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 30 Jan 2026 21:06:49 +1000 Subject: [PATCH 12/41] gen(AWSRequest): Unpick some more of the logic --- gen/src/Gen/AST/Data.hs | 22 +++++++- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 69 +++++++++++++++-------- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 3eb05d1dcdc..74719ca89f8 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -8,6 +8,7 @@ module Gen.AST.Data ) where +import Control.Comonad (extract) import qualified Control.Lens as Lens import qualified Control.Monad.Trans.State as State import qualified Data.ByteString.Char8 as ByteString.Char8 @@ -57,19 +58,34 @@ operationData cfg m o = do ToElement {} -> Just "XML" _ -> Nothing + responseParser + | null ys = AWSRequest.ParseNull + | isShared . extract $ yr ^. refAnn, + all fieldBody ys = + let wrapper = yr ^. refResultWrapper + in case m ^. protocol of + APIGateway -> AWSRequest.ParseAllJSON + JSON -> AWSRequest.ParseAllJSON + RestJSON -> AWSRequest.ParseAllJSON + EC2 -> AWSRequest.ParseAllXML wrapper + Query -> AWSRequest.ParseAllXML wrapper + RestXML -> AWSRequest.ParseAllXML wrapper + | otherwise = AWSRequest.FigureItOut + cls <- pp Print $ AWSRequest.instanceD AWSRequest.Config { requestType = identifier xr, - requestFunction, - responseType = identifier yr, -- Lookup a specific operationPlugins key before checking -- for the wildcard. - operationPlugins = + requestOperationPlugins = fromMaybe [] $ (cfg ^. operationPlugins . Lens.at (identifier xr)) <|> (cfg ^. operationPlugins . Lens.at (mkId "*")), + requestFunction, + responseType = identifier yr, + responseParser, serviceConfig = m ^. serviceConfig } (m ^. metadata) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index d6919fc0a20..28918139227 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -27,7 +27,7 @@ import Gen.AST.Data.Syntax ) import Gen.Prelude import qualified Gen.Protocol as Proto -import Gen.Types hiding (Config, operationPlugins, serviceConfig) +import Gen.Types hiding (Config, serviceConfig) import qualified Language.Haskell.Exts as Exts data Config = Config @@ -39,15 +39,30 @@ data Config = Config requestFunction :: Text, -- | List of function names to apply to the computed request in -- the implementation of the @request@ function. - operationPlugins :: [Text], + requestOperationPlugins :: [Text], -- | Name of the data type we generate for the botocore output -- shape that corresponds to 'requestType'. responseType :: Id, + -- | How to parse the response from AWS. + responseParser :: ResponseParser, -- | Name of the service config value to use by default. As of -- 2025-11, always @"defaultService"@; the parser for 'Metadata' -- would override the service abbrev in all cases. serviceConfig :: Text } + deriving (Show) + +-- | How to generate the parser for AWS's response to an API call. +data ResponseParser + = -- | Perform no parsing and return an empty response. The response + -- constructor in 'responseType' must take no arguments. + ParseNull + | -- | Parse the entire response as JSON. + ParseAllJSON + | -- | Parse the entire resposne as XML, with an optional wrapper. + ParseAllXML (Maybe Text) + | FigureItOut + deriving (Show) instanceD :: Config -> @@ -59,7 +74,7 @@ instanceD c@Config {requestType} m (responseRef, responseFields) = Just [ awsResponseD c, requestD c, - funD "response" (responseE (m ^. protocol) responseRef responseFields) + funD "response" (responseE c (m ^. protocol) responseRef responseFields) ] awsResponseD :: Config -> Exts.InstDecl () @@ -71,7 +86,7 @@ awsResponseD Config {..} = requestD :: Config -> Exts.InstDecl () requestD Config {..} = - funArgsD "request" ["overrides"] $ foldr applyPlugin e operationPlugins + funArgsD "request" ["overrides"] $ foldr applyPlugin e requestOperationPlugins where -- Plugin functions are of the form :: Request a -> Request a applyPlugin x = Exts.infixApp (var x) "Prelude.." @@ -81,17 +96,26 @@ requestD Config {..} = (var $ "Request." <> requestFunction) (Exts.app (var "overrides") (var serviceConfig)) -responseE :: Protocol -> Ref -> [Field] -> Exts.Exp () -responseE p r fs = Exts.app (responseF p r fs) bdy +responseE :: Config -> Protocol -> Ref -> [Field] -> Exts.Exp () +responseE Config {..} p r fs = + case responseParser of + ParseNull -> var "Response.receiveNull" `Exts.app` var (ctorId responseType) + ParseAllJSON -> + var "Response.receiveJSON" + `Exts.app` lam (var "Data.eitherParseJSON" `Exts.app` var "x") + ParseAllXML Nothing -> + var "Response.receiveXML" + `Exts.app` lam (var "Data.parseXML" `Exts.app` var "x") + ParseAllXML (Just wrapper) -> + var "Response.receiveXMLWrapper" + `Exts.app` str wrapper + `Exts.app` lam (var "Data.parseXML" `Exts.app` var "x") + FigureItOut -> Exts.app responseF bdy where n = r ^. Lens.to identifier - s = r ^. refAnn . Lens.to Comonad.extract bdy :: Exts.Exp () - bdy - | null fs = var (ctorId n) - | isShared s, all fieldBody fs = lam parseAll - | otherwise = lam . ctorE n $ map parseField fs + bdy = lam . ctorE n $ map parseField fs lam :: Exts.Exp () -> Exts.Exp () lam = Exts.lamE [Exts.pvar "s", Exts.pvar "h", Exts.pvar "x"] @@ -145,18 +169,17 @@ responseE p r fs = Exts.app (responseF p r fs) bdy body = any fieldStream fs --- FIXME: take method into account for responses, such as HEAD etc, particuarly --- when the body might be totally empty. -responseF :: Protocol -> RefF a -> [Field] -> Exts.Exp () -responseF p r fs - | null fs = var "Response.receiveNull" - | any fieldStream fs = var "Response.receiveBody" - | any fieldLitPayload fs = var "Response.receiveBytes" - | Just x <- r ^. refResultWrapper = Exts.app (var (suf <> "Wrapper")) (str x) - | not $ any fieldBody fs = var "Response.receiveEmpty" - | otherwise = var suf - where - suf = "Response.receive" <> Proto.suffix p + -- FIXME: take method into account for responses, such as HEAD + -- etc, particuarly when the body might be totally empty. + responseF :: Exts.Exp () + responseF + | any fieldStream fs = var "Response.receiveBody" + | any fieldLitPayload fs = var "Response.receiveBytes" + | Just x <- r ^. refResultWrapper = Exts.app (var (suf <> "Wrapper")) (str x) + | not $ any fieldBody fs = var "Response.receiveEmpty" + | otherwise = var suf + where + suf = "Response.receive" <> Proto.suffix p parseHeadersE :: Protocol -> Field -> Exts.Exp () parseHeadersE p f From 38e1133a460f967791378959fb83484df715a480 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 14 Mar 2026 23:36:31 +0000 Subject: [PATCH 13/41] gen: Small tidyups --- gen/amazonka-gen.cabal | 2 +- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/gen/amazonka-gen.cabal b/gen/amazonka-gen.cabal index 2d73e2d30f2..3c91421ee6e 100644 --- a/gen/amazonka-gen.cabal +++ b/gen/amazonka-gen.cabal @@ -125,13 +125,13 @@ library Gen.Types.URI Gen.Types.Waiter Gen.WordFrequency - Paths_amazonka_gen executable gen import: base hs-source-dirs: bin main-is: gen.hs ghc-options: -threaded -rtsopts "-with-rtsopts=-A128m -I0 -qg0" + other-modules: Paths_amazonka_gen build-depends: , amazonka-gen , filepath diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 28918139227..adc50063c93 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -1,7 +1,5 @@ module Gen.AST.Data.Syntax.AWSRequest where -import qualified Control.Comonad as Comonad -import qualified Control.Lens as Lens import Gen.AST.Data.Field (Field, fieldBody, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) import Gen.AST.Data.Syntax ( ctorE, @@ -112,10 +110,8 @@ responseE Config {..} p r fs = `Exts.app` lam (var "Data.parseXML" `Exts.app` var "x") FigureItOut -> Exts.app responseF bdy where - n = r ^. Lens.to identifier - bdy :: Exts.Exp () - bdy = lam . ctorE n $ map parseField fs + bdy = lam . ctorE (identifier r) $ map parseField fs lam :: Exts.Exp () -> Exts.Exp () lam = Exts.lamE [Exts.pvar "s", Exts.pvar "h", Exts.pvar "x"] From 65105284e712312024c9b96b79c8c3d3f12ff59b Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 14 Mar 2026 23:53:04 +0000 Subject: [PATCH 14/41] amazonka-core/gen: Rename `receiveBody` -> `receiveStreamingBody` --- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 2 +- lib/amazonka-core/src/Amazonka/Response.hs | 44 ++++++++++++++-------- lib/amazonka/CHANGELOG.md | 3 +- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index adc50063c93..6aa0b73bd9c 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -169,7 +169,7 @@ responseE Config {..} p r fs = -- etc, particuarly when the body might be totally empty. responseF :: Exts.Exp () responseF - | any fieldStream fs = var "Response.receiveBody" + | any fieldStream fs = var "Response.receiveStreamingBody" | any fieldLitPayload fs = var "Response.receiveBytes" | Just x <- r ^. refResultWrapper = Exts.app (var (suf <> "Wrapper")) (str x) | not $ any fieldBody fs = var "Response.receiveEmpty" diff --git a/lib/amazonka-core/src/Amazonka/Response.hs b/lib/amazonka-core/src/Amazonka/Response.hs index 442db1a7dff..eec55875c6f 100644 --- a/lib/amazonka-core/src/Amazonka/Response.hs +++ b/lib/amazonka-core/src/Amazonka/Response.hs @@ -10,10 +10,11 @@ -- the connection. This is needed to avoid hitting this issue: -- . -- --- The only exception is 'receiveBody', which passes a streaming response --- body to a callback and thus is not allowed to close the connection. Users --- of streaming functions are advised to be careful and consume the response --- body manually if they want the connection to be closed promptly. +-- The only exception is 'receiveStreamingBody', which passes a streaming +-- response body to a callback and thus is not allowed to close the +-- connection. Users of streaming functions are advised to be careful and +-- consume the response body manually if they want the connection to be closed +-- promptly. -- -- Note that using 'runResourceT' will always close the connection. module Amazonka.Response @@ -24,6 +25,7 @@ module Amazonka.Response receiveJSON, receiveBytes, receiveBody, + receiveStreamingBody, ) where @@ -43,7 +45,7 @@ import Network.HTTP.Types (ResponseHeaders) import qualified Text.XML as XML receiveNull :: - MonadResource m => + (MonadResource m) => AWSResponse a -> (ByteStringLazy -> IO ByteStringLazy) -> Service -> @@ -55,7 +57,7 @@ receiveNull rs _ = liftIO (Client.responseClose r) $> Right rs receiveEmpty :: - MonadResource m => + (MonadResource m) => (Int -> ResponseHeaders -> () -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> Service -> @@ -67,7 +69,7 @@ receiveEmpty f _ = liftIO (Client.responseClose r) $> f s h () receiveXMLWrapper :: - MonadResource m => + (MonadResource m) => Text -> (Int -> ResponseHeaders -> [XML.Node] -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> @@ -78,7 +80,7 @@ receiveXMLWrapper :: receiveXMLWrapper n f = receiveXML (\s h x -> x .@ n >>= f s h) receiveXML :: - MonadResource m => + (MonadResource m) => (Int -> ResponseHeaders -> [XML.Node] -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> Service -> @@ -88,7 +90,7 @@ receiveXML :: receiveXML = deserialise decodeXML receiveJSON :: - MonadResource m => + (MonadResource m) => (Int -> ResponseHeaders -> Aeson.Object -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> Service -> @@ -98,7 +100,7 @@ receiveJSON :: receiveJSON = deserialise Aeson.eitherDecode' receiveBytes :: - MonadResource m => + (MonadResource m) => (Int -> ResponseHeaders -> ByteString -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> Service -> @@ -107,21 +109,33 @@ receiveBytes :: m (Either Error (ClientResponse (AWSResponse a))) receiveBytes = deserialise (Right . LBS.toStrict) +-- | Deprecated alias for 'receiveStreamingBody'. receiveBody :: - MonadResource m => + (MonadResource m) => (Int -> ResponseHeaders -> ResponseBody -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> Service -> Proxy a -> ClientResponse ClientBody -> m (Either Error (ClientResponse (AWSResponse a))) -receiveBody f _ = +receiveBody = receiveStreamingBody +{-# DEPRECATED receiveBody "this function will be removed in Amazonka 2.2" #-} + +receiveStreamingBody :: + (MonadResource m) => + (Int -> ResponseHeaders -> ResponseBody -> Either String (AWSResponse a)) -> + (ByteStringLazy -> IO ByteStringLazy) -> + Service -> + Proxy a -> + ClientResponse ClientBody -> + m (Either Error (ClientResponse (AWSResponse a))) +receiveStreamingBody f _ = stream $ \_ s h x -> pure (f s h (ResponseBody x)) -- | Deserialise an entire response body, such as an XML or JSON payload. deserialise :: - MonadResource m => + (MonadResource m) => (ByteStringLazy -> Either String b) -> (Int -> ResponseHeaders -> b -> Either String (AWSResponse a)) -> (ByteStringLazy -> IO ByteStringLazy) -> @@ -146,7 +160,7 @@ deserialise reader parser responseBodyHook Service {..} _ rs = -- | Stream a raw response body, such as an S3 object payload. stream :: - MonadResource m => + (MonadResource m) => ( ClientResponse () -> Int -> ResponseHeaders -> @@ -173,5 +187,5 @@ stream parser Service {..} _ rs = Except.throwE $ SerializeError (SerializeError' abbrev status Nothing err) -sinkLBS :: MonadResource m => ClientBody -> m ByteStringLazy +sinkLBS :: (MonadResource m) => ClientBody -> m ByteStringLazy sinkLBS bdy = liftResourceT (bdy `Conduit.connect` Conduit.Binary.sinkLbs) diff --git a/lib/amazonka/CHANGELOG.md b/lib/amazonka/CHANGELOG.md index c328efc7045..d7ab040e716 100644 --- a/lib/amazonka/CHANGELOG.md +++ b/lib/amazonka/CHANGELOG.md @@ -122,7 +122,8 @@ [\#955](https://github.com/brendanhay/amazonka/pull/955) - `amazonka-core`: Support Cloudflare R2 by adjusting which headers get signed [\#977](https://github.com/brendanhay/amazonka/pull/977) - +- `amazonka-core`: `Amazonka.Response.receiveBody` has been renamed to `Amazonka.Response.receiveStreamingBody`. This is an internal function used in service bindings and should not affect normal users. `Amazonka.Response.receiveBody` will remain as a deprecated alias until Amazonka 2.2. +[\#???](https://github.com/brendanhay/amazonka/pull/???) ## [2.0.0](https://github.com/brendanhay/amazonka/tree/2.0.0) Released: **28 July 2023**, Compare: [2.0.0-rc2](https://github.com/brendanhay/amazonka/compare/2.0.0-rc2...2.0.0) From 82de6c51a34196941f9ca2229f6a74aa2c3047db Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:08 +0000 Subject: [PATCH 15/41] amazonka-backupstorage: regenerating service --- .../gen/Amazonka/BackupStorage/GetChunk.hs | 2 +- .../gen/Amazonka/BackupStorage/GetObjectMetadata.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetChunk.hs b/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetChunk.hs index f502be11f54..25420c204ce 100644 --- a/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetChunk.hs +++ b/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetChunk.hs @@ -96,7 +96,7 @@ instance Core.AWSRequest GetChunk where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetChunkResponse' Prelude.<$> (Prelude.pure (Prelude.fromEnum s)) diff --git a/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetObjectMetadata.hs b/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetObjectMetadata.hs index 81763f91650..dc208250fbc 100644 --- a/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetObjectMetadata.hs +++ b/lib/services/amazonka-backupstorage/gen/Amazonka/BackupStorage/GetObjectMetadata.hs @@ -99,7 +99,7 @@ instance Core.AWSRequest GetObjectMetadata where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetObjectMetadataResponse' Prelude.<$> (h Data..#? "x-amz-checksum") From 9d496e7d022c1a00f1587f05025587aa0b72b32e Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:12 +0000 Subject: [PATCH 16/41] amazonka-codeartifact: regenerating service --- .../gen/Amazonka/CodeArtifact/GetPackageVersionAsset.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-codeartifact/gen/Amazonka/CodeArtifact/GetPackageVersionAsset.hs b/lib/services/amazonka-codeartifact/gen/Amazonka/CodeArtifact/GetPackageVersionAsset.hs index ea3e50f4d85..238c8d80aeb 100644 --- a/lib/services/amazonka-codeartifact/gen/Amazonka/CodeArtifact/GetPackageVersionAsset.hs +++ b/lib/services/amazonka-codeartifact/gen/Amazonka/CodeArtifact/GetPackageVersionAsset.hs @@ -230,7 +230,7 @@ instance Core.AWSRequest GetPackageVersionAsset where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetPackageVersionAssetResponse' Prelude.<$> (h Data..#? "X-AssetName") From 3d3d8a3d3e7958e708615adf729eb7c41e58a524 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:18 +0000 Subject: [PATCH 17/41] amazonka-ebs: regenerating service --- lib/services/amazonka-ebs/gen/Amazonka/EBS/GetSnapshotBlock.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-ebs/gen/Amazonka/EBS/GetSnapshotBlock.hs b/lib/services/amazonka-ebs/gen/Amazonka/EBS/GetSnapshotBlock.hs index 3bda84a6af5..e77ad0d8d93 100644 --- a/lib/services/amazonka-ebs/gen/Amazonka/EBS/GetSnapshotBlock.hs +++ b/lib/services/amazonka-ebs/gen/Amazonka/EBS/GetSnapshotBlock.hs @@ -149,7 +149,7 @@ instance Core.AWSRequest GetSnapshotBlock where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetSnapshotBlockResponse' Prelude.<$> (h Data..#? "x-amz-Checksum") From 2ad86cd0a360e3dd32c5c1d75573b14890e5fdf6 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:23 +0000 Subject: [PATCH 18/41] amazonka-glacier: regenerating service --- .../amazonka-glacier/gen/Amazonka/Glacier/GetJobOutput.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-glacier/gen/Amazonka/Glacier/GetJobOutput.hs b/lib/services/amazonka-glacier/gen/Amazonka/Glacier/GetJobOutput.hs index ad091371bf9..e686f3889f4 100644 --- a/lib/services/amazonka-glacier/gen/Amazonka/Glacier/GetJobOutput.hs +++ b/lib/services/amazonka-glacier/gen/Amazonka/Glacier/GetJobOutput.hs @@ -271,7 +271,7 @@ instance Core.AWSRequest GetJobOutput where Request.glacierVersionHeader (Core.version defaultService) Prelude.. Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetJobOutputResponse' Prelude.<$> (h Data..#? "Accept-Ranges") From 744b0b592046c479c26189c8670716dc62b5ff76 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:29 +0000 Subject: [PATCH 19/41] amazonka-kinesis-video-archived-media: regenerating service --- .../gen/Amazonka/KinesisVideoArchivedMedia/GetClip.hs | 2 +- .../KinesisVideoArchivedMedia/GetMediaForFragmentList.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetClip.hs b/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetClip.hs index 6b9d924f421..e9a11f735f5 100644 --- a/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetClip.hs +++ b/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetClip.hs @@ -159,7 +159,7 @@ instance Core.AWSRequest GetClip where request overrides = Request.postJSON (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetClipResponse' Prelude.<$> (h Data..#? "Content-Type") diff --git a/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetMediaForFragmentList.hs b/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetMediaForFragmentList.hs index 48d493b668b..e399361ade5 100644 --- a/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetMediaForFragmentList.hs +++ b/lib/services/amazonka-kinesis-video-archived-media/gen/Amazonka/KinesisVideoArchivedMedia/GetMediaForFragmentList.hs @@ -146,7 +146,7 @@ instance Core.AWSRequest GetMediaForFragmentList where request overrides = Request.postJSON (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetMediaForFragmentListResponse' Prelude.<$> (h Data..#? "Content-Type") From aa4158344fe937356176ba172b4a6ab1eefc7086 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:29 +0000 Subject: [PATCH 20/41] amazonka-kinesis-video-media: regenerating service --- .../gen/Amazonka/KinesisVideoMedia/GetMedia.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-kinesis-video-media/gen/Amazonka/KinesisVideoMedia/GetMedia.hs b/lib/services/amazonka-kinesis-video-media/gen/Amazonka/KinesisVideoMedia/GetMedia.hs index 3df189de94b..3b01f71d818 100644 --- a/lib/services/amazonka-kinesis-video-media/gen/Amazonka/KinesisVideoMedia/GetMedia.hs +++ b/lib/services/amazonka-kinesis-video-media/gen/Amazonka/KinesisVideoMedia/GetMedia.hs @@ -154,7 +154,7 @@ instance Core.AWSRequest GetMedia where request overrides = Request.postJSON (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetMediaResponse' Prelude.<$> (h Data..#? "Content-Type") From 13e56558ca32c639a76a510009ace606dbc55dea Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:30 +0000 Subject: [PATCH 21/41] amazonka-lakeformation: regenerating service --- .../gen/Amazonka/LakeFormation/GetWorkUnitResults.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-lakeformation/gen/Amazonka/LakeFormation/GetWorkUnitResults.hs b/lib/services/amazonka-lakeformation/gen/Amazonka/LakeFormation/GetWorkUnitResults.hs index ca2c6da7031..f2718d5f423 100644 --- a/lib/services/amazonka-lakeformation/gen/Amazonka/LakeFormation/GetWorkUnitResults.hs +++ b/lib/services/amazonka-lakeformation/gen/Amazonka/LakeFormation/GetWorkUnitResults.hs @@ -121,7 +121,7 @@ instance Core.AWSRequest GetWorkUnitResults where request overrides = Request.postJSON (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetWorkUnitResultsResponse' Prelude.<$> (Prelude.pure (Prelude.fromEnum s)) From 538d36572ea72a72a78d77c1f81ec1ca88c1380e Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:30 +0000 Subject: [PATCH 22/41] amazonka-lex-runtime: regenerating service --- .../amazonka-lex-runtime/gen/Amazonka/LexRuntime/PostContent.hs | 2 +- .../amazonka-lex-runtime/gen/Amazonka/LexRuntime/PutSession.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PostContent.hs b/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PostContent.hs index 80edcb506a8..74429b506e3 100644 --- a/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PostContent.hs +++ b/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PostContent.hs @@ -553,7 +553,7 @@ instance Core.AWSRequest PostContent where request overrides = Request.postBody (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> PostContentResponse' Prelude.<$> (h Data..#? "x-amz-lex-active-contexts") diff --git a/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PutSession.hs b/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PutSession.hs index 0904a178b8f..613b8c1f6db 100644 --- a/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PutSession.hs +++ b/lib/services/amazonka-lex-runtime/gen/Amazonka/LexRuntime/PutSession.hs @@ -333,7 +333,7 @@ instance Core.AWSRequest PutSession where request overrides = Request.postJSON (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> PutSessionResponse' Prelude.<$> (h Data..#? "x-amz-lex-active-contexts") From 5aefa1dfe4fb356136beaaa44dbcf0d3d28efaa6 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:34 +0000 Subject: [PATCH 23/41] amazonka-medialive: regenerating service --- .../gen/Amazonka/MediaLive/DescribeInputDeviceThumbnail.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-medialive/gen/Amazonka/MediaLive/DescribeInputDeviceThumbnail.hs b/lib/services/amazonka-medialive/gen/Amazonka/MediaLive/DescribeInputDeviceThumbnail.hs index c479891f1d5..061fbd2ea85 100644 --- a/lib/services/amazonka-medialive/gen/Amazonka/MediaLive/DescribeInputDeviceThumbnail.hs +++ b/lib/services/amazonka-medialive/gen/Amazonka/MediaLive/DescribeInputDeviceThumbnail.hs @@ -104,7 +104,7 @@ instance Core.AWSRequest DescribeInputDeviceThumbnail where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> DescribeInputDeviceThumbnailResponse' Prelude.<$> (h Data..#? "Content-Length") From fca88047a33fb09771045dcc0f856e34b19210bd Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:34 +0000 Subject: [PATCH 24/41] amazonka-mediastore-dataplane: regenerating service --- .../gen/Amazonka/MediaStoreData/GetObject.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-mediastore-dataplane/gen/Amazonka/MediaStoreData/GetObject.hs b/lib/services/amazonka-mediastore-dataplane/gen/Amazonka/MediaStoreData/GetObject.hs index 956d0656049..8e65c35cb66 100644 --- a/lib/services/amazonka-mediastore-dataplane/gen/Amazonka/MediaStoreData/GetObject.hs +++ b/lib/services/amazonka-mediastore-dataplane/gen/Amazonka/MediaStoreData/GetObject.hs @@ -183,7 +183,7 @@ instance Core.AWSRequest GetObject where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetObjectResponse' Prelude.<$> (h Data..#? "Cache-Control") From 7f54b48cc93d4e619df1f092aa6c78c0f1b5ec26 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:37 +0000 Subject: [PATCH 25/41] amazonka-omics: regenerating service --- lib/services/amazonka-omics/gen/Amazonka/Omics/GetReadSet.hs | 2 +- lib/services/amazonka-omics/gen/Amazonka/Omics/GetReference.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReadSet.hs b/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReadSet.hs index 810c507a495..1452a969b45 100644 --- a/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReadSet.hs +++ b/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReadSet.hs @@ -115,7 +115,7 @@ instance Core.AWSRequest GetReadSet where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetReadSetResponse' Prelude.<$> (Prelude.pure (Prelude.fromEnum s)) diff --git a/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReference.hs b/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReference.hs index fd2d64fd27b..707906bdec4 100644 --- a/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReference.hs +++ b/lib/services/amazonka-omics/gen/Amazonka/Omics/GetReference.hs @@ -125,7 +125,7 @@ instance Core.AWSRequest GetReference where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetReferenceResponse' Prelude.<$> (Prelude.pure (Prelude.fromEnum s)) From f021ebf85dfa31cdf9da96279863b3ef330e7c80 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:39 +0000 Subject: [PATCH 26/41] amazonka-polly: regenerating service --- .../amazonka-polly/gen/Amazonka/Polly/SynthesizeSpeech.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-polly/gen/Amazonka/Polly/SynthesizeSpeech.hs b/lib/services/amazonka-polly/gen/Amazonka/Polly/SynthesizeSpeech.hs index 04fd2fd6dba..6c5e0831cc1 100644 --- a/lib/services/amazonka-polly/gen/Amazonka/Polly/SynthesizeSpeech.hs +++ b/lib/services/amazonka-polly/gen/Amazonka/Polly/SynthesizeSpeech.hs @@ -333,7 +333,7 @@ instance Core.AWSRequest SynthesizeSpeech where request overrides = Request.postJSON (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> SynthesizeSpeechResponse' Prelude.<$> (h Data..#? "Content-Type") From d9144af409e4564fd4c427b4347be66c14f5fcda Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:44 +0000 Subject: [PATCH 27/41] amazonka-s3: regenerating service --- lib/services/amazonka-s3/gen/Amazonka/S3/GetObject.hs | 2 +- lib/services/amazonka-s3/gen/Amazonka/S3/GetObjectTorrent.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/amazonka-s3/gen/Amazonka/S3/GetObject.hs b/lib/services/amazonka-s3/gen/Amazonka/S3/GetObject.hs index 1cef8bc552d..50efcfdc12c 100644 --- a/lib/services/amazonka-s3/gen/Amazonka/S3/GetObject.hs +++ b/lib/services/amazonka-s3/gen/Amazonka/S3/GetObject.hs @@ -585,7 +585,7 @@ instance Core.AWSRequest GetObject where Request.s3vhost Prelude.. Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetObjectResponse' Prelude.<$> (h Data..#? "accept-ranges") diff --git a/lib/services/amazonka-s3/gen/Amazonka/S3/GetObjectTorrent.hs b/lib/services/amazonka-s3/gen/Amazonka/S3/GetObjectTorrent.hs index c1227f44cfc..68a36abd064 100644 --- a/lib/services/amazonka-s3/gen/Amazonka/S3/GetObjectTorrent.hs +++ b/lib/services/amazonka-s3/gen/Amazonka/S3/GetObjectTorrent.hs @@ -141,7 +141,7 @@ instance Core.AWSRequest GetObjectTorrent where Request.s3vhost Prelude.. Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetObjectTorrentResponse' Prelude.<$> (h Data..#? "x-amz-request-charged") From ba61dc60574674c8cf1715a5f3be0377e3c37ed8 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:45 +0000 Subject: [PATCH 28/41] amazonka-sagemaker-geospatial: regenerating service --- .../gen/Amazonka/SageMakerGeoSpatial/GetTile.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-sagemaker-geospatial/gen/Amazonka/SageMakerGeoSpatial/GetTile.hs b/lib/services/amazonka-sagemaker-geospatial/gen/Amazonka/SageMakerGeoSpatial/GetTile.hs index 9ae3d8e2276..7f8337d53c4 100644 --- a/lib/services/amazonka-sagemaker-geospatial/gen/Amazonka/SageMakerGeoSpatial/GetTile.hs +++ b/lib/services/amazonka-sagemaker-geospatial/gen/Amazonka/SageMakerGeoSpatial/GetTile.hs @@ -197,7 +197,7 @@ instance Core.AWSRequest GetTile where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetTileResponse' Prelude.<$> (Prelude.pure (Prelude.fromEnum s)) From 06229e3847529976cef20c8e918b5314d05b98c0 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 07:17:53 +0000 Subject: [PATCH 29/41] amazonka-workmailmessageflow: regenerating service --- .../gen/Amazonka/WorkMailMessageFlow/GetRawMessageContent.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/amazonka-workmailmessageflow/gen/Amazonka/WorkMailMessageFlow/GetRawMessageContent.hs b/lib/services/amazonka-workmailmessageflow/gen/Amazonka/WorkMailMessageFlow/GetRawMessageContent.hs index 3bc70a6a4e3..a530604fa38 100644 --- a/lib/services/amazonka-workmailmessageflow/gen/Amazonka/WorkMailMessageFlow/GetRawMessageContent.hs +++ b/lib/services/amazonka-workmailmessageflow/gen/Amazonka/WorkMailMessageFlow/GetRawMessageContent.hs @@ -82,7 +82,7 @@ instance Core.AWSRequest GetRawMessageContent where request overrides = Request.get (overrides defaultService) response = - Response.receiveBody + Response.receiveStreamingBody ( \s h x -> GetRawMessageContentResponse' Prelude.<$> (Prelude.pure (Prelude.fromEnum s)) From 0ce8e078f2451b46d0a89e48498de4bfd8a9132a Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Fri, 20 Mar 2026 09:13:44 +0000 Subject: [PATCH 30/41] gen: Chip `ParseStreamingBody` out from `AWSRequest` --- gen/src/Gen/AST/Data.hs | 4 +++ gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 31 +++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 74719ca89f8..c1606595de4 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -70,8 +70,12 @@ operationData cfg m o = do EC2 -> AWSRequest.ParseAllXML wrapper Query -> AWSRequest.ParseAllXML wrapper RestXML -> AWSRequest.ParseAllXML wrapper + | any fieldStream ys = + AWSRequest.ParseStreamingBody responseFieldParsers | otherwise = AWSRequest.FigureItOut + responseFieldParsers = AWSRequest.FigureTheFieldOut <$> ys + cls <- pp Print $ AWSRequest.instanceD diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 6aa0b73bd9c..62ecf699e2e 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -59,9 +59,18 @@ data ResponseParser ParseAllJSON | -- | Parse the entire resposne as XML, with an optional wrapper. ParseAllXML (Maybe Text) + | -- | Parse a streaming response from AWS into a + -- 'Amazonka.Body.ResponseBody'. + ParseStreamingBody [ResponseFieldParser] | FigureItOut deriving (Show) +-- | How to generate the parser for a single field, for response +-- parsers which do per-field parsing. +newtype ResponseFieldParser + = FigureTheFieldOut Field + deriving (Show) + instanceD :: Config -> Metadata f -> @@ -108,16 +117,23 @@ responseE Config {..} p r fs = var "Response.receiveXMLWrapper" `Exts.app` str wrapper `Exts.app` lam (var "Data.parseXML" `Exts.app` var "x") + ParseStreamingBody fieldParsers -> + var "Response.receiveStreamingBody" + `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) FigureItOut -> Exts.app responseF bdy where bdy :: Exts.Exp () - bdy = lam . ctorE (identifier r) $ map parseField fs + bdy = lam . ctorE (identifier r) $ map parseField' fs lam :: Exts.Exp () -> Exts.Exp () lam = Exts.lamE [Exts.pvar "s", Exts.pvar "h", Exts.pvar "x"] - parseField :: Field -> Exts.Exp () - parseField x = + parseField :: ResponseFieldParser -> Exts.Exp () + parseField = \case + FigureTheFieldOut field -> parseField' field + + parseField' :: Field -> Exts.Exp () + parseField' x = case fieldLocation x of Just Headers -> parseHeadersE p x Just Header -> parseHeadersE p x @@ -169,11 +185,10 @@ responseE Config {..} p r fs = -- etc, particuarly when the body might be totally empty. responseF :: Exts.Exp () responseF - | any fieldStream fs = var "Response.receiveStreamingBody" - | any fieldLitPayload fs = var "Response.receiveBytes" - | Just x <- r ^. refResultWrapper = Exts.app (var (suf <> "Wrapper")) (str x) - | not $ any fieldBody fs = var "Response.receiveEmpty" - | otherwise = var suf + | any fieldLitPayload fs = trace "responseF(fieldLitPayload)" $ var "Response.receiveBytes" + | Just x <- r ^. refResultWrapper = trace "responseF(Wrapper)" $ Exts.app (var (suf <> "Wrapper")) (str x) + | not $ any fieldBody fs = trace "responseF(receiveEmpty)" $ var "Response.receiveEmpty" + | otherwise = trace "responseF(var suf)" $ var suf where suf = "Response.receive" <> Proto.suffix p From 463aaf202bacefe31cfd85a2fa16da967a10e098 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 28 Mar 2026 05:42:55 +0000 Subject: [PATCH 31/41] gen: simplify generation of header parsing --- gen/src/Gen/AST/Data/Syntax.hs | 7 +------ gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 20 ++++++++------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/gen/src/Gen/AST/Data/Syntax.hs b/gen/src/Gen/AST/Data/Syntax.hs index 1d455e4be34..a05e6d31638 100644 --- a/gen/src/Gen/AST/Data/Syntax.hs +++ b/gen/src/Gen/AST/Data/Syntax.hs @@ -58,15 +58,10 @@ pJE = "Data..:>" pJEMay = "Data..?>" pJEDef = pXDef -pH, pHMay :: QOp -pH = "Data..#" -pHMay = "Data..#?" - -pXMap, pXList, pXList1, pHMap :: Exp +pXMap, pXList, pXList1 :: Exp pXMap = var "Data.parseXMLMap" pXList = var "Data.parseXMLList" pXList1 = var "Data.parseXMLList1" -pHMap = var "Data.parseHeadersMap" toX, toXAttr, toJ, toQ, toH :: QOp toX = "Data.@=" diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 62ecf699e2e..0aacdc30894 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -3,15 +3,11 @@ module Gen.AST.Data.Syntax.AWSRequest where import Gen.AST.Data.Field (Field, fieldBody, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) import Gen.AST.Data.Syntax ( ctorE, - decodeE, funArgsD, funD, instD, justE, memberName, - pH, - pHMap, - pHMay, pJE, pJEDef, pJEMay, @@ -135,8 +131,8 @@ responseE Config {..} p r fs = parseField' :: Field -> Exts.Exp () parseField' x = case fieldLocation x of - Just Headers -> parseHeadersE p x - Just Header -> parseHeadersE p x + Just Headers -> parseHeadersE (memberName p Output x) (typeOf x) + Just Header -> parseHeadersE (memberName p Output x) (typeOf x) Just StatusCode -> parseStatusE x Just Body | body -> Exts.app pureE (var "x") Nothing | body -> Exts.app pureE (var "x") @@ -192,13 +188,13 @@ responseE Config {..} p r fs = where suf = "Response.receive" <> Proto.suffix p -parseHeadersE :: Protocol -> Field -> Exts.Exp () -parseHeadersE p f - | TMap {} <- typeOf f = Exts.appFun pHMap [str n, h] - | fieldMaybe f = decodeE h pHMay n - | otherwise = decodeE h pH n +parseHeadersE :: Text -> TType -> Exts.Exp () +parseHeadersE headerName = \case + TMap {} -> Exts.appFun (var "Data.parseHeadersMap") [n, h] + TMaybe {} -> Exts.infixApp h "Data..#?" n + _ -> Exts.infixApp h "Data..#" n where - n = memberName p Output f + n = str headerName h = var "h" parseStatusE :: Field -> Exts.Exp () From 62641228cc2e068520f98ef729b219e550f59191 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 28 Mar 2026 07:21:22 +0000 Subject: [PATCH 32/41] gen: Define field parser for parsing response headers --- gen/src/Gen/AST/Data.hs | 22 ++++++++++++-- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 35 +++++++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index c1606595de4..aeee50b6d4b 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -71,10 +71,28 @@ operationData cfg m o = do Query -> AWSRequest.ParseAllXML wrapper RestXML -> AWSRequest.ParseAllXML wrapper | any fieldStream ys = - AWSRequest.ParseStreamingBody responseFieldParsers + AWSRequest.ParseStreamingBody responseFieldParsers | otherwise = AWSRequest.FigureItOut - responseFieldParsers = AWSRequest.FigureTheFieldOut <$> ys + responseFieldParsers = + ys <&> \f -> + case fieldLocation f of + -- TODO: Unify the 'Headers' and 'Header' location constructors. + Just Headers -> AWSRequest.ParseHeaderField hName hParser + where + hName = memberName (m ^. protocol) Output f + hParser = case typeOf f of + TMap {} -> AWSRequest.HeaderFieldMap + TMaybe {} -> AWSRequest.HeaderFieldOptional + _ -> AWSRequest.HeaderFieldRequired + Just Header -> AWSRequest.ParseHeaderField hName hParser + where + hName = memberName (m ^. protocol) Output f + hParser = case typeOf f of + TMap {} -> AWSRequest.HeaderFieldMap + TMaybe {} -> AWSRequest.HeaderFieldOptional + _ -> AWSRequest.HeaderFieldRequired + _ -> AWSRequest.FigureTheFieldOut f cls <- pp Print $ diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 0aacdc30894..49a32a6a47b 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -1,4 +1,11 @@ -module Gen.AST.Data.Syntax.AWSRequest where +module Gen.AST.Data.Syntax.AWSRequest + ( instanceD, + Config (..), + ResponseParser (..), + ResponseFieldParser (..), + HeaderFieldParser (..), + ) +where import Gen.AST.Data.Field (Field, fieldBody, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) import Gen.AST.Data.Syntax @@ -63,8 +70,25 @@ data ResponseParser -- | How to generate the parser for a single field, for response -- parsers which do per-field parsing. -newtype ResponseFieldParser - = FigureTheFieldOut Field +data ResponseFieldParser + = -- | Parse the repsonse field from the HTTP response headers named + -- like the given 'Text'. + ParseHeaderField Text HeaderFieldParser + | FigureTheFieldOut Field + deriving (Show) + +-- | How to parse a single field from the HTTP response headers. +-- +-- A 'HeaderFieldParser' does not parse a single header, because some +-- AWS services represent structured data by breaking it across +-- multiple similarly-named headers. +data HeaderFieldParser + = -- | Parse a required field from headers using @(.#)@. + HeaderFieldRequired + | -- | Parse an optional field from headers using @(.#?)@. + HeaderFieldOptional + | -- | Parse a map field from headers using @parseHeadersMap@. + HeaderFieldMap deriving (Show) instanceD :: @@ -126,6 +150,11 @@ responseE Config {..} p r fs = parseField :: ResponseFieldParser -> Exts.Exp () parseField = \case + ParseHeaderField hName hField -> case hField of + HeaderFieldRequired -> Exts.infixApp (var "h") "Data..#" (str hName) + HeaderFieldOptional -> Exts.infixApp (var "h") "Data..#?" (str hName) + HeaderFieldMap -> + Exts.appFun (var "Data.parseHeadersMap") [str hName, var "h"] FigureTheFieldOut field -> parseField' field parseField' :: Field -> Exts.Exp () From d6fabea75c66b5ad5a7586f6ff4471237b8f454f Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 28 Mar 2026 09:25:58 +0000 Subject: [PATCH 33/41] gen: Simplify `Location` type --- gen/src/Gen/AST/Data.hs | 8 -------- gen/src/Gen/AST/Data/Field.hs | 10 ++-------- gen/src/Gen/AST/Data/Instance.hs | 4 ++-- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 1 - gen/src/Gen/Types/Service.hs | 18 +++++++++++------- 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index aeee50b6d4b..5d3aae10e02 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -77,7 +77,6 @@ operationData cfg m o = do responseFieldParsers = ys <&> \f -> case fieldLocation f of - -- TODO: Unify the 'Headers' and 'Header' location constructors. Just Headers -> AWSRequest.ParseHeaderField hName hParser where hName = memberName (m ^. protocol) Output f @@ -85,13 +84,6 @@ operationData cfg m o = do TMap {} -> AWSRequest.HeaderFieldMap TMaybe {} -> AWSRequest.HeaderFieldOptional _ -> AWSRequest.HeaderFieldRequired - Just Header -> AWSRequest.ParseHeaderField hName hParser - where - hName = memberName (m ^. protocol) Output f - hParser = case typeOf f of - TMap {} -> AWSRequest.HeaderFieldMap - TMaybe {} -> AWSRequest.HeaderFieldOptional - _ -> AWSRequest.HeaderFieldRequired _ -> AWSRequest.FigureTheFieldOut f cls <- diff --git a/gen/src/Gen/AST/Data/Field.hs b/gen/src/Gen/AST/Data/Field.hs index 6480790fc84..7db1489dbc3 100644 --- a/gen/src/Gen/AST/Data/Field.hs +++ b/gen/src/Gen/AST/Data/Field.hs @@ -50,13 +50,7 @@ instance TypeOf Field where TList {} -> True _ -> False - isHeader = - fieldLocation f - `elem` map - Just - [ Headers, - Header - ] + isHeader = fieldLocation f == Just Headers ref = f ^. fieldRef typ = fmap unBase64 (typeOf ref) @@ -71,7 +65,7 @@ instance HasInfo Field where -- FIXME: Can just add the metadata to field as well since -- the protocol/timestamp are passed in everywhere in the .Syntax module. mkFields :: - HasMetadata a Identity => + (HasMetadata a Identity) => a -> Solved -> StructF (Shape Solved) -> diff --git a/gen/src/Gen/AST/Data/Instance.hs b/gen/src/Gen/AST/Data/Instance.hs index 7f74294f541..cdb07318f34 100644 --- a/gen/src/Gen/AST/Data/Instance.hs +++ b/gen/src/Gen/AST/Data/Instance.hs @@ -130,7 +130,7 @@ requestInsts m oname h r fs = do where merged xs = let ys = - map Right (satisfies [Querystring] fs) <> xs + map Right (satisfies [QueryString] fs) <> xs ++ map Left protocolQuery in case List.find f is of Just (ToQuery zs) -> ToQuery (ys <> zs) @@ -217,7 +217,7 @@ requestInsts m oname h r fs = do v = ("Version", Just version) headers :: [Field] - headers = satisfies [Header, Headers] fs + headers = [f | f <- fs, fieldLocation f == Just Headers] target = (<> ("." <> action)) <$> m ^. targetPrefix diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 49a32a6a47b..1cd64850177 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -161,7 +161,6 @@ responseE Config {..} p r fs = parseField' x = case fieldLocation x of Just Headers -> parseHeadersE (memberName p Output x) (typeOf x) - Just Header -> parseHeadersE (memberName p Output x) (typeOf x) Just StatusCode -> parseStatusE x Just Body | body -> Exts.app pureE (var "x") Nothing | body -> Exts.app pureE (var "x") diff --git a/gen/src/Gen/Types/Service.hs b/gen/src/Gen/Types/Service.hs index 7dfd215aea1..03acbcf93d3 100644 --- a/gen/src/Gen/Types/Service.hs +++ b/gen/src/Gen/Types/Service.hs @@ -97,18 +97,22 @@ instance ToJSON Checksum where data Location = Headers - | Header | Uri - | Querystring + | QueryString | StatusCode - | Body + | -- | Not present in @botocore@, but we explicitly annotate fields + -- with it. + Body deriving stock (Eq, Show, Generic) instance FromJSON Location where - parseJSON = gParseJSON' camel - -instance ToJSON Location where - toJSON = gToJSON' camel + parseJSON = Aeson.withText "Location" $ \t -> case t of + "header" -> pure Headers -- Present in e.g. accessanalyzer + "headers" -> pure Headers -- Present in e.g. dataexchange + "querystring" -> pure QueryString + "statusCode" -> pure StatusCode + "uri" -> pure Uri + _ -> fail $ "Unknown location: " <> show t data XML = XML' { _xmlPrefix :: Maybe Text, From c36eec75a5db653c33d0377b77e0423126e48530 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sat, 28 Mar 2026 10:54:44 +0000 Subject: [PATCH 34/41] gen: Rename receiver function type --- gen/src/Gen/AST/Data.hs | 20 +++++----- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 46 ++++++++++++----------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 5d3aae10e02..34dfeec5e12 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -58,20 +58,20 @@ operationData cfg m o = do ToElement {} -> Just "XML" _ -> Nothing - responseParser - | null ys = AWSRequest.ParseNull + responseReceiver + | null ys = AWSRequest.ReceiveNull | isShared . extract $ yr ^. refAnn, all fieldBody ys = let wrapper = yr ^. refResultWrapper in case m ^. protocol of - APIGateway -> AWSRequest.ParseAllJSON - JSON -> AWSRequest.ParseAllJSON - RestJSON -> AWSRequest.ParseAllJSON - EC2 -> AWSRequest.ParseAllXML wrapper - Query -> AWSRequest.ParseAllXML wrapper - RestXML -> AWSRequest.ParseAllXML wrapper + APIGateway -> AWSRequest.ReceiveJsonAll + JSON -> AWSRequest.ReceiveJsonAll + RestJSON -> AWSRequest.ReceiveJsonAll + EC2 -> AWSRequest.ReceiveXmlAll wrapper + Query -> AWSRequest.ReceiveXmlAll wrapper + RestXML -> AWSRequest.ReceiveXmlAll wrapper | any fieldStream ys = - AWSRequest.ParseStreamingBody responseFieldParsers + AWSRequest.ReceiveStreamingBody responseFieldParsers | otherwise = AWSRequest.FigureItOut responseFieldParsers = @@ -99,7 +99,7 @@ operationData cfg m o = do <|> (cfg ^. operationPlugins . Lens.at (mkId "*")), requestFunction, responseType = identifier yr, - responseParser, + responseReceiver, serviceConfig = m ^. serviceConfig } (m ^. metadata) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 1cd64850177..43a281c1ad6 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -1,7 +1,7 @@ module Gen.AST.Data.Syntax.AWSRequest ( instanceD, Config (..), - ResponseParser (..), + ResponseReceiver (..), ResponseFieldParser (..), HeaderFieldParser (..), ) @@ -44,8 +44,8 @@ data Config = Config -- | Name of the data type we generate for the botocore output -- shape that corresponds to 'requestType'. responseType :: Id, - -- | How to parse the response from AWS. - responseParser :: ResponseParser, + -- | How to receive and parse the response from AWS. + responseReceiver :: ResponseReceiver, -- | Name of the service config value to use by default. As of -- 2025-11, always @"defaultService"@; the parser for 'Metadata' -- would override the service abbrev in all cases. @@ -53,18 +53,21 @@ data Config = Config } deriving (Show) --- | How to generate the parser for AWS's response to an API call. -data ResponseParser - = -- | Perform no parsing and return an empty response. The response - -- constructor in 'responseType' must take no arguments. - ParseNull - | -- | Parse the entire response as JSON. - ParseAllJSON - | -- | Parse the entire resposne as XML, with an optional wrapper. - ParseAllXML (Maybe Text) - | -- | Parse a streaming response from AWS into a - -- 'Amazonka.Body.ResponseBody'. - ParseStreamingBody [ResponseFieldParser] +-- | Which Amazonka function should be used to parse AWS's response to +-- an API call, and additional arguments are necessary to actually +-- perform the parse. +data ResponseReceiver + = -- | Perform no parsing and return an empty response using + -- @receiveNull@. The response constructor in 'responseType' must + -- take no arguments. + ReceiveNull + | -- | Parse the entire response as JSON using @receiveJSON@. + ReceiveJsonAll + | -- | Parse the entire response as (optionally wrapped) XML, using + -- @receiveXML@ or @receiveXMLWrapper@. + ReceiveXmlAll (Maybe Text) + | -- | Parse a streaming response from AWS using @receiveStreamingBody@. + ReceiveStreamingBody [ResponseFieldParser] | FigureItOut deriving (Show) @@ -125,19 +128,20 @@ requestD Config {..} = responseE :: Config -> Protocol -> Ref -> [Field] -> Exts.Exp () responseE Config {..} p r fs = - case responseParser of - ParseNull -> var "Response.receiveNull" `Exts.app` var (ctorId responseType) - ParseAllJSON -> + case responseReceiver of + ReceiveNull -> + var "Response.receiveNull" `Exts.app` var (ctorId responseType) + ReceiveJsonAll -> var "Response.receiveJSON" `Exts.app` lam (var "Data.eitherParseJSON" `Exts.app` var "x") - ParseAllXML Nothing -> + ReceiveXmlAll Nothing -> var "Response.receiveXML" `Exts.app` lam (var "Data.parseXML" `Exts.app` var "x") - ParseAllXML (Just wrapper) -> + ReceiveXmlAll (Just wrapper) -> var "Response.receiveXMLWrapper" `Exts.app` str wrapper `Exts.app` lam (var "Data.parseXML" `Exts.app` var "x") - ParseStreamingBody fieldParsers -> + ReceiveStreamingBody fieldParsers -> var "Response.receiveStreamingBody" `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) FigureItOut -> Exts.app responseF bdy From 47cfa66c2bfee98bb2448aa2e2f512044d8c7e05 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Sun, 29 Mar 2026 12:07:55 +0000 Subject: [PATCH 35/41] gen: Add explicit constructor for `receiveBytes` parsing --- gen/src/Gen/AST/Data.hs | 2 ++ gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 34dfeec5e12..205ffd96ee8 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -72,6 +72,8 @@ operationData cfg m o = do RestXML -> AWSRequest.ReceiveXmlAll wrapper | any fieldStream ys = AWSRequest.ReceiveStreamingBody responseFieldParsers + | any fieldLitPayload ys = + AWSRequest.ReceiveBytes responseFieldParsers | otherwise = AWSRequest.FigureItOut responseFieldParsers = diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 43a281c1ad6..88856bec4ce 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -68,6 +68,9 @@ data ResponseReceiver ReceiveXmlAll (Maybe Text) | -- | Parse a streaming response from AWS using @receiveStreamingBody@. ReceiveStreamingBody [ResponseFieldParser] + | -- | Parse a response from AWS, accepting the body as an unparsed + -- 'ByteString'. Uses @receiveBytes@. + ReceiveBytes [ResponseFieldParser] | FigureItOut deriving (Show) @@ -144,6 +147,9 @@ responseE Config {..} p r fs = ReceiveStreamingBody fieldParsers -> var "Response.receiveStreamingBody" `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) + ReceiveBytes fieldParsers -> + var "Response.receiveBytes" + `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) FigureItOut -> Exts.app responseF bdy where bdy :: Exts.Exp () @@ -213,7 +219,6 @@ responseE Config {..} p r fs = -- etc, particuarly when the body might be totally empty. responseF :: Exts.Exp () responseF - | any fieldLitPayload fs = trace "responseF(fieldLitPayload)" $ var "Response.receiveBytes" | Just x <- r ^. refResultWrapper = trace "responseF(Wrapper)" $ Exts.app (var (suf <> "Wrapper")) (str x) | not $ any fieldBody fs = trace "responseF(receiveEmpty)" $ var "Response.receiveEmpty" | otherwise = trace "responseF(var suf)" $ var suf From 66b8f81939ccc4305cae5b1df0a3d0931b065a7b Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Tue, 31 Mar 2026 11:19:32 +0000 Subject: [PATCH 36/41] gen: Chip out parsing of fieldwise (wrapped) XML --- gen/src/Gen/AST/Data.hs | 30 ++++++++++++++++------- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 11 ++++++++- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 205ffd96ee8..3ec676652b0 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -61,20 +61,32 @@ operationData cfg m o = do responseReceiver | null ys = AWSRequest.ReceiveNull | isShared . extract $ yr ^. refAnn, - all fieldBody ys = - let wrapper = yr ^. refResultWrapper - in case m ^. protocol of - APIGateway -> AWSRequest.ReceiveJsonAll - JSON -> AWSRequest.ReceiveJsonAll - RestJSON -> AWSRequest.ReceiveJsonAll - EC2 -> AWSRequest.ReceiveXmlAll wrapper - Query -> AWSRequest.ReceiveXmlAll wrapper - RestXML -> AWSRequest.ReceiveXmlAll wrapper + all fieldBody ys = case m ^. protocol of + APIGateway -> AWSRequest.ReceiveJsonAll + JSON -> AWSRequest.ReceiveJsonAll + RestJSON -> AWSRequest.ReceiveJsonAll + EC2 -> AWSRequest.ReceiveXmlAll wrapper + Query -> AWSRequest.ReceiveXmlAll wrapper + RestXML -> AWSRequest.ReceiveXmlAll wrapper | any fieldStream ys = AWSRequest.ReceiveStreamingBody responseFieldParsers | any fieldLitPayload ys = AWSRequest.ReceiveBytes responseFieldParsers + | -- Check if we should parse wrapped XML before considering + -- ReceiveEmpty, because fieldBody is not true for fields + -- parsed from within wrapped XML. + isXml && isJust wrapper = + AWSRequest.ReceiveXml wrapper responseFieldParsers | otherwise = AWSRequest.FigureItOut + where + wrapper = yr ^. refResultWrapper + isXml = case m ^. protocol of + APIGateway -> False + JSON -> False + RestJSON -> False + EC2 -> True + Query -> True + RestXML -> True responseFieldParsers = ys <&> \f -> diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 88856bec4ce..82774183818 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -71,6 +71,9 @@ data ResponseReceiver | -- | Parse a response from AWS, accepting the body as an unparsed -- 'ByteString'. Uses @receiveBytes@. ReceiveBytes [ResponseFieldParser] + | -- | Parse (optionally wrapped) XML response by field using either + -- @receiveXML@ or @receiveXMLWrapper@. + ReceiveXml (Maybe Text) [ResponseFieldParser] | FigureItOut deriving (Show) @@ -150,6 +153,13 @@ responseE Config {..} p r fs = ReceiveBytes fieldParsers -> var "Response.receiveBytes" `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) + ReceiveXml Nothing fieldParsers -> + var "Response.receiveXML" + `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) + ReceiveXml (Just wrapper) fieldParsers -> + var "Response.receiveXMLWrapper" + `Exts.app` str wrapper + `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) FigureItOut -> Exts.app responseF bdy where bdy :: Exts.Exp () @@ -219,7 +229,6 @@ responseE Config {..} p r fs = -- etc, particuarly when the body might be totally empty. responseF :: Exts.Exp () responseF - | Just x <- r ^. refResultWrapper = trace "responseF(Wrapper)" $ Exts.app (var (suf <> "Wrapper")) (str x) | not $ any fieldBody fs = trace "responseF(receiveEmpty)" $ var "Response.receiveEmpty" | otherwise = trace "responseF(var suf)" $ var suf where From 055e8f15950215da637509f8e34810f5e5e18b0a Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Tue, 31 Mar 2026 11:29:14 +0000 Subject: [PATCH 37/41] gen: Chip out check for receiveEmpty --- gen/src/Gen/AST/Data.hs | 2 ++ gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 3ec676652b0..dc29d334e5a 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -77,6 +77,8 @@ operationData cfg m o = do -- parsed from within wrapped XML. isXml && isJust wrapper = AWSRequest.ReceiveXml wrapper responseFieldParsers + | not $ any fieldBody ys = + AWSRequest.ReceiveEmpty responseFieldParsers | otherwise = AWSRequest.FigureItOut where wrapper = yr ^. refResultWrapper diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 82774183818..6e5d8803074 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -7,7 +7,7 @@ module Gen.AST.Data.Syntax.AWSRequest ) where -import Gen.AST.Data.Field (Field, fieldBody, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) +import Gen.AST.Data.Field (Field, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) import Gen.AST.Data.Syntax ( ctorE, funArgsD, @@ -74,6 +74,10 @@ data ResponseReceiver | -- | Parse (optionally wrapped) XML response by field using either -- @receiveXML@ or @receiveXMLWrapper@. ReceiveXml (Maybe Text) [ResponseFieldParser] + | -- | Parse an empty response using @receiveEmpty@. The body will + -- not be available to parse and all fields will come from headers + -- or status. + ReceiveEmpty [ResponseFieldParser] | FigureItOut deriving (Show) @@ -160,6 +164,9 @@ responseE Config {..} p r fs = var "Response.receiveXMLWrapper" `Exts.app` str wrapper `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) + ReceiveEmpty fieldParsers -> + var "Response.receiveEmpty" + `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) FigureItOut -> Exts.app responseF bdy where bdy :: Exts.Exp () @@ -228,9 +235,7 @@ responseE Config {..} p r fs = -- FIXME: take method into account for responses, such as HEAD -- etc, particuarly when the body might be totally empty. responseF :: Exts.Exp () - responseF - | not $ any fieldBody fs = trace "responseF(receiveEmpty)" $ var "Response.receiveEmpty" - | otherwise = trace "responseF(var suf)" $ var suf + responseF = trace "responseF(var suf)" $ var suf where suf = "Response.receive" <> Proto.suffix p From f255c3deeffe0b0aa6bc0ad84c7c2397456b098d Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 23 Apr 2026 10:42:07 +0000 Subject: [PATCH 38/41] gen: Chip out fieldwise JSON parsing --- gen/src/Gen/AST/Data.hs | 11 +++++++++++ gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index dc29d334e5a..02e176119e4 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -79,9 +79,20 @@ operationData cfg m o = do AWSRequest.ReceiveXml wrapper responseFieldParsers | not $ any fieldBody ys = AWSRequest.ReceiveEmpty responseFieldParsers + | isJson = + AWSRequest.ReceiveJson responseFieldParsers | otherwise = AWSRequest.FigureItOut where wrapper = yr ^. refResultWrapper + + isJson = case m ^. protocol of + APIGateway -> True + JSON -> True + RestJSON -> True + EC2 -> False + Query -> False + RestXML -> False + isXml = case m ^. protocol of APIGateway -> False JSON -> False diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 6e5d8803074..8713c590c94 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -71,7 +71,9 @@ data ResponseReceiver | -- | Parse a response from AWS, accepting the body as an unparsed -- 'ByteString'. Uses @receiveBytes@. ReceiveBytes [ResponseFieldParser] - | -- | Parse (optionally wrapped) XML response by field using either + | -- | Parse a JSON response from AWS fieldwise using @receiveJSON@. + ReceiveJson [ResponseFieldParser] + | -- | Parse (optionally wrapped) XML response fieldwise using either -- @receiveXML@ or @receiveXMLWrapper@. ReceiveXml (Maybe Text) [ResponseFieldParser] | -- | Parse an empty response using @receiveEmpty@. The body will @@ -157,6 +159,9 @@ responseE Config {..} p r fs = ReceiveBytes fieldParsers -> var "Response.receiveBytes" `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) + ReceiveJson fieldParsers -> + var "Response.receiveJSON" + `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) ReceiveXml Nothing fieldParsers -> var "Response.receiveXML" `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) From b45c03e77f665eaa21f75673390eef94464a39d3 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 23 Apr 2026 11:15:29 +0000 Subject: [PATCH 39/41] gen: Complete the removal of `responseF` --- gen/src/Gen/AST/Data.hs | 17 +++++++++--- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 34 ++++------------------- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 02e176119e4..3f988f8536b 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -68,20 +68,29 @@ operationData cfg m o = do EC2 -> AWSRequest.ReceiveXmlAll wrapper Query -> AWSRequest.ReceiveXmlAll wrapper RestXML -> AWSRequest.ReceiveXmlAll wrapper + -- FIXME: take method into account for responses, such as HEAD + -- etc, particuarly when the body might be totally empty. | any fieldStream ys = AWSRequest.ReceiveStreamingBody responseFieldParsers | any fieldLitPayload ys = AWSRequest.ReceiveBytes responseFieldParsers | -- Check if we should parse wrapped XML before considering - -- ReceiveEmpty, because fieldBody is not true for fields + -- ReceiveEmpty, because fieldBody is false for fields -- parsed from within wrapped XML. isXml && isJust wrapper = AWSRequest.ReceiveXml wrapper responseFieldParsers - | not $ any fieldBody ys = + | -- Then check for responses that don't use the field body, + -- to avoid trying to `receiveXml` on API calls where AWS + -- might send us an empty body. + not $ any fieldBody ys = AWSRequest.ReceiveEmpty responseFieldParsers + | -- Finally, check for unwrapped XML, parsed fieldwise. + isXml && isNothing wrapper = + AWSRequest.ReceiveXml wrapper responseFieldParsers | isJson = AWSRequest.ReceiveJson responseFieldParsers - | otherwise = AWSRequest.FigureItOut + | otherwise = + error "Gen.AST.Data.operationData(responseReceiver): don't know how to parse" where wrapper = yr ^. refResultWrapper @@ -130,7 +139,7 @@ operationData cfg m o = do serviceConfig = m ^. serviceConfig } (m ^. metadata) - (yr, ys) + ys mpage <- pagerFields m o >>= traverse (pp Print . pagerD xn) yis' <- renderInsts p yn (responseInsts ys) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index 8713c590c94..cc01193643f 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -14,7 +14,6 @@ import Gen.AST.Data.Syntax funD, instD, justE, - memberName, pJE, pJEDef, pJEMay, @@ -27,7 +26,6 @@ import Gen.AST.Data.Syntax var, ) import Gen.Prelude -import qualified Gen.Protocol as Proto import Gen.Types hiding (Config, serviceConfig) import qualified Language.Haskell.Exts as Exts @@ -80,7 +78,6 @@ data ResponseReceiver -- not be available to parse and all fields will come from headers -- or status. ReceiveEmpty [ResponseFieldParser] - | FigureItOut deriving (Show) -- | How to generate the parser for a single field, for response @@ -109,14 +106,14 @@ data HeaderFieldParser instanceD :: Config -> Metadata f -> - (Ref, [Field]) -> + [Field] -> Exts.Decl () -instanceD c@Config {requestType} m (responseRef, responseFields) = +instanceD c@Config {requestType} m responseFields = instD "Core.AWSRequest" requestType $ Just [ awsResponseD c, requestD c, - funD "response" (responseE c (m ^. protocol) responseRef responseFields) + funD "response" (responseE c (m ^. protocol) responseFields) ] awsResponseD :: Config -> Exts.InstDecl () @@ -138,8 +135,8 @@ requestD Config {..} = (var $ "Request." <> requestFunction) (Exts.app (var "overrides") (var serviceConfig)) -responseE :: Config -> Protocol -> Ref -> [Field] -> Exts.Exp () -responseE Config {..} p r fs = +responseE :: Config -> Protocol -> [Field] -> Exts.Exp () +responseE Config {..} p fs = case responseReceiver of ReceiveNull -> var "Response.receiveNull" `Exts.app` var (ctorId responseType) @@ -172,11 +169,7 @@ responseE Config {..} p r fs = ReceiveEmpty fieldParsers -> var "Response.receiveEmpty" `Exts.app` lam (ctorE responseType $ map parseField fieldParsers) - FigureItOut -> Exts.app responseF bdy where - bdy :: Exts.Exp () - bdy = lam . ctorE (identifier r) $ map parseField' fs - lam :: Exts.Exp () -> Exts.Exp () lam = Exts.lamE [Exts.pvar "s", Exts.pvar "h", Exts.pvar "x"] @@ -192,7 +185,6 @@ responseE Config {..} p r fs = parseField' :: Field -> Exts.Exp () parseField' x = case fieldLocation x of - Just Headers -> parseHeadersE (memberName p Output x) (typeOf x) Just StatusCode -> parseStatusE x Just Body | body -> Exts.app pureE (var "x") Nothing | body -> Exts.app pureE (var "x") @@ -237,22 +229,6 @@ responseE Config {..} p r fs = body = any fieldStream fs - -- FIXME: take method into account for responses, such as HEAD - -- etc, particuarly when the body might be totally empty. - responseF :: Exts.Exp () - responseF = trace "responseF(var suf)" $ var suf - where - suf = "Response.receive" <> Proto.suffix p - -parseHeadersE :: Text -> TType -> Exts.Exp () -parseHeadersE headerName = \case - TMap {} -> Exts.appFun (var "Data.parseHeadersMap") [n, h] - TMaybe {} -> Exts.infixApp h "Data..#?" n - _ -> Exts.infixApp h "Data..#" n - where - n = str headerName - h = var "h" - parseStatusE :: Field -> Exts.Exp () parseStatusE f | fieldMaybe f = Exts.app pureE (Exts.app justE v) From 5e0510bd99481ae89a79f72e33d5f51a71a371fb Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 23 Apr 2026 11:22:50 +0000 Subject: [PATCH 40/41] gen: Remove dead code in `parseStatusE` and inline The guard for `fieldMaybe` is never hit in any model --- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index cc01193643f..a2be25bf9a6 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -7,7 +7,7 @@ module Gen.AST.Data.Syntax.AWSRequest ) where -import Gen.AST.Data.Field (Field, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldMaybe, fieldPayload, fieldStream) +import Gen.AST.Data.Field (Field, fieldIsParam, fieldLit, fieldLitPayload, fieldLocation, fieldPayload, fieldStream) import Gen.AST.Data.Syntax ( ctorE, funArgsD, @@ -185,7 +185,8 @@ responseE Config {..} p fs = parseField' :: Field -> Exts.Exp () parseField' x = case fieldLocation x of - Just StatusCode -> parseStatusE x + Just StatusCode -> + pureE `Exts.app` Exts.paren (var "Prelude.fromEnum" `Exts.app` var "s") Just Body | body -> Exts.app pureE (var "x") Nothing | body -> Exts.app pureE (var "x") _ -> parseProto x @@ -228,10 +229,3 @@ responseE Config {..} p fs = _ -> var "Data.parseXML" body = any fieldStream fs - -parseStatusE :: Field -> Exts.Exp () -parseStatusE f - | fieldMaybe f = Exts.app pureE (Exts.app justE v) - | otherwise = Exts.app pureE v - where - v = Exts.paren $ Exts.app (var "Prelude.fromEnum") (var "s") From 9e7315b08ebd88b04420dcc09c92c46b4db1d803 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 23 Apr 2026 11:26:35 +0000 Subject: [PATCH 41/41] fixup! gen: Define field parser for parsing response headers --- gen/src/Gen/AST/Data.hs | 2 +- gen/src/Gen/AST/Data/Syntax/AWSRequest.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gen/src/Gen/AST/Data.hs b/gen/src/Gen/AST/Data.hs index 3f988f8536b..e9b6df628f0 100644 --- a/gen/src/Gen/AST/Data.hs +++ b/gen/src/Gen/AST/Data.hs @@ -113,7 +113,7 @@ operationData cfg m o = do responseFieldParsers = ys <&> \f -> case fieldLocation f of - Just Headers -> AWSRequest.ParseHeaderField hName hParser + Just Headers -> AWSRequest.ParseHeader hName hParser where hName = memberName (m ^. protocol) Output f hParser = case typeOf f of diff --git a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs index a2be25bf9a6..391e5e53a5b 100644 --- a/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs +++ b/gen/src/Gen/AST/Data/Syntax/AWSRequest.hs @@ -85,7 +85,7 @@ data ResponseReceiver data ResponseFieldParser = -- | Parse the repsonse field from the HTTP response headers named -- like the given 'Text'. - ParseHeaderField Text HeaderFieldParser + ParseHeader Text HeaderFieldParser | FigureTheFieldOut Field deriving (Show) @@ -175,7 +175,7 @@ responseE Config {..} p fs = parseField :: ResponseFieldParser -> Exts.Exp () parseField = \case - ParseHeaderField hName hField -> case hField of + ParseHeader hName hField -> case hField of HeaderFieldRequired -> Exts.infixApp (var "h") "Data..#" (str hName) HeaderFieldOptional -> Exts.infixApp (var "h") "Data..#?" (str hName) HeaderFieldMap ->