@@ -15,6 +15,7 @@ import (
1515
1616 "github.com/getkin/kin-openapi/openapi3"
1717 "github.com/getkin/kin-openapi/openapi3filter"
18+ "github.com/stackql/any-sdk/pkg/casing"
1819 "github.com/stackql/any-sdk/pkg/dto"
1920 "github.com/stackql/any-sdk/pkg/fuzzymatch"
2021 "github.com/stackql/any-sdk/pkg/media"
@@ -64,6 +65,8 @@ type OperationStore interface {
6465 GetInverse () (OperationInverse , bool )
6566 GetStackQLConfig () StackQLConfig
6667 GetQueryParamPushdown () (QueryParamPushdown , bool )
68+ GetRequestNativeCasing () string
69+ GetParameterOrError (paramKey string ) (Addressable , error )
6770 GetRetryPolicy () RetryPolicy
6871 GetParameters () map [string ]Addressable
6972 GetPathItem () * openapi3.PathItem
@@ -1050,10 +1053,10 @@ func (m *standardOpenAPIOperationStore) getRequestBodySchemaAttributeMatcher(pat
10501053 return nil , fmt .Errorf ("could not find schema at path '%s'" , path )
10511054 }
10521055 }
1053- return getschemaAttributeMatcher (schemaOfInterest )
1056+ return getschemaAttributeMatcher (schemaOfInterest , m . GetRequestNativeCasing () )
10541057}
10551058
1056- func getschemaAttributeMatcher (schemaOfInterest Schema ) (fuzzymatch.FuzzyMatcher [string ], error ) {
1059+ func getschemaAttributeMatcher (schemaOfInterest Schema , nativeCasing string ) (fuzzymatch.FuzzyMatcher [string ], error ) {
10571060 var matchers []fuzzymatch.StringFuzzyPair
10581061 for k := range schemaOfInterest .getProperties () {
10591062 if k == "" {
@@ -1065,6 +1068,17 @@ func getschemaAttributeMatcher(schemaOfInterest Schema) (fuzzymatch.FuzzyMatcher
10651068 return nil , regexpErr
10661069 }
10671070 matchers = append (matchers , fuzzymatch .NewFuzzyPair (keyRegexp , k ))
1071+ // When a native wire casing is declared, also accept the snake_case form of
1072+ // the wire property name and map it back to the wire key.
1073+ if nativeCasing != "" {
1074+ if snakeKey := casing .ToSnake (k ); snakeKey != k {
1075+ snakeRegexp , snakeErr := regexp .Compile (fmt .Sprintf ("^%s$" , regexp .QuoteMeta (snakeKey )))
1076+ if snakeErr != nil {
1077+ return nil , snakeErr
1078+ }
1079+ matchers = append (matchers , fuzzymatch .NewFuzzyPair (snakeRegexp , k ))
1080+ }
1081+ }
10681082 }
10691083 return fuzzymatch .NewRegexpStringMetcher (matchers ), nil
10701084}
@@ -1284,10 +1298,44 @@ func (m *standardOpenAPIOperationStore) GetNonBodyParameters() map[string]Addres
12841298 return m .getNonBodyParameters ()
12851299}
12861300
1301+ func (m * standardOpenAPIOperationStore ) GetRequestNativeCasing () string {
1302+ if m .Request != nil {
1303+ return m .Request .GetNativeCasing ()
1304+ }
1305+ return ""
1306+ }
1307+
12871308func (m * standardOpenAPIOperationStore ) GetParameter (paramKey string ) (Addressable , bool ) {
12881309 params := m .GetParameters ()
1289- rv , ok := params [paramKey ]
1290- return rv , ok
1310+ if rv , ok := params [paramKey ]; ok {
1311+ return rv , true
1312+ }
1313+ // Reverse-casing retry: when the method declares a native wire casing, convert
1314+ // the (snake_case) SQL key to that casing and retry. Absent native casing this
1315+ // is a no-op, preserving existing behaviour.
1316+ if nc := m .GetRequestNativeCasing (); nc != "" {
1317+ if wireKey := casing .FromSnake (paramKey , nc ); wireKey != paramKey {
1318+ if rv , ok := params [wireKey ]; ok {
1319+ return rv , true
1320+ }
1321+ }
1322+ }
1323+ return nil , false
1324+ }
1325+
1326+ // GetParameterOrError resolves a parameter (including reverse-casing) and, on a
1327+ // final miss, returns a *ParameterNotFoundError listing the wire-format names.
1328+ func (m * standardOpenAPIOperationStore ) GetParameterOrError (paramKey string ) (Addressable , error ) {
1329+ if rv , ok := m .GetParameter (paramKey ); ok {
1330+ return rv , nil
1331+ }
1332+ params := m .GetParameters ()
1333+ wireNames := make ([]string , 0 , len (params ))
1334+ for k := range params {
1335+ wireNames = append (wireNames , k )
1336+ }
1337+ sort .Strings (wireNames )
1338+ return nil , & ParameterNotFoundError {Key : paramKey , AvailableWireNames : wireNames }
12911339}
12921340
12931341func (m * standardOpenAPIOperationStore ) GetName () string {
@@ -1819,10 +1867,21 @@ func (op *standardOpenAPIOperationStore) getOverridenResponse(httpResponse *http
18191867 overrideMediaType := expectedResponse .GetOverrrideBodyMediaType ()
18201868 if responseTransformExists {
18211869 input := string (bodyBytes )
1822- streamTransformerFactory := stream_transform .NewStreamTransformerFactory (
1823- responseTransform .GetType (),
1824- responseTransform .GetBody (),
1825- )
1870+ var streamTransformerFactory stream_transform.StreamTransformerFactory
1871+ if responseTransform .GetType () == stream_transform .SchemaDrivenXMLV1 {
1872+ listProperty := strings .TrimPrefix (expectedResponse .GetObjectKey (), "$." )
1873+ streamTransformerFactory = stream_transform .NewSchemaDrivenXMLStreamTransformerFactory (
1874+ responseTransform .GetType (),
1875+ newXMLSchemaAdapter (expectedResponse .GetSchema ()),
1876+ op .getXProtocol (),
1877+ listProperty ,
1878+ )
1879+ } else {
1880+ streamTransformerFactory = stream_transform .NewStreamTransformerFactory (
1881+ responseTransform .GetType (),
1882+ responseTransform .GetBody (),
1883+ )
1884+ }
18261885 if ! streamTransformerFactory .IsTransformable () {
18271886 return nil , fmt .Errorf ("unsupported template type: %s" , responseTransform .GetType ())
18281887 }
@@ -1848,6 +1907,68 @@ func (op *standardOpenAPIOperationStore) getOverridenResponse(httpResponse *http
18481907 return nil , fmt .Errorf ("unprocessable response body for operation = %s" , op .GetName ())
18491908}
18501909
1910+ // getXProtocol reads the info-level x-protocol hint (query|ec2|rest-xml) used by
1911+ // the schema_driven_xml transform to skip the right response envelope.
1912+ func (op * standardOpenAPIOperationStore ) getXProtocol () string {
1913+ if op .OpenAPIService == nil {
1914+ return ""
1915+ }
1916+ t := op .OpenAPIService .getT ()
1917+ if t == nil || t .Info == nil {
1918+ return ""
1919+ }
1920+ v , err := extractExtensionValBytes (t .Info .Extensions , ExtensionKeyProtocol )
1921+ if err != nil {
1922+ return ""
1923+ }
1924+ return strings .Trim (strings .TrimSpace (string (v )), `"` )
1925+ }
1926+
1927+ // xmlSchemaAdapter adapts an internal Schema to stream_transform.SchemaTree so the
1928+ // schema_driven_xml walker can navigate it without importing internal/anysdk.
1929+ type xmlSchemaAdapter struct {
1930+ s Schema
1931+ }
1932+
1933+ func newXMLSchemaAdapter (s Schema ) stream_transform.SchemaTree {
1934+ if s == nil {
1935+ return nil
1936+ }
1937+ return xmlSchemaAdapter {s : s }
1938+ }
1939+
1940+ func (a xmlSchemaAdapter ) Type () string {
1941+ return a .s .GetType ()
1942+ }
1943+
1944+ func (a xmlSchemaAdapter ) Items () (stream_transform.SchemaTree , bool ) {
1945+ items , err := a .s .GetItemsSchema ()
1946+ if err != nil || items == nil {
1947+ return nil , false
1948+ }
1949+ return xmlSchemaAdapter {s : items }, true
1950+ }
1951+
1952+ func (a xmlSchemaAdapter ) Property (name string ) (stream_transform.SchemaTree , bool ) {
1953+ p , ok := a .s .GetProperty (name )
1954+ if ! ok || p == nil {
1955+ return nil , false
1956+ }
1957+ return xmlSchemaAdapter {s : p }, true
1958+ }
1959+
1960+ func (a xmlSchemaAdapter ) Properties () map [string ]stream_transform.SchemaTree {
1961+ props , err := a .s .GetProperties ()
1962+ if err != nil {
1963+ return nil
1964+ }
1965+ out := make (map [string ]stream_transform.SchemaTree , len (props ))
1966+ for k , v := range props {
1967+ out [k ] = xmlSchemaAdapter {s : v }
1968+ }
1969+ return out
1970+ }
1971+
18511972func (op * standardOpenAPIOperationStore ) ProcessResponse (httpResponse * http.Response ) (ProcessedOperationResponse , error ) {
18521973 responseSchema , mediaType , err := op .GetResponseBodySchemaAndMediaType ()
18531974 if err != nil {
0 commit comments