1717
1818use compact_str:: CompactString ;
1919use serde:: { Deserialize , Serialize } ;
20+ use serde_json:: Value as JsonValue ;
2021
2122use super :: super :: is_default;
23+ use crate :: config:: common:: GenericError ;
24+ use crate :: typed_struct:: TypedStructFilter ;
2225
2326/// Set Filter State HTTP filter configuration
24- ///
27+ ///
2528/// This filter dynamically sets filter state objects based on request data.
2629/// Filter state can be used for routing decisions, metadata propagation,
2730/// and internal connection handling.
@@ -36,33 +39,33 @@ pub struct SetFilterState {
3639#[ derive( Debug , Clone , Deserialize , Serialize , PartialEq , Eq ) ]
3740pub struct FilterStateValue {
3841 /// Filter state object key (required)
39- ///
42+ ///
4043 /// Examples:
4144 /// - "io.istio.connect_authority" (Istio HBONE)
4245 /// - "envoy.filters.listener.original_dst.local_ip"
4346 /// - "envoy.tcp_proxy.cluster"
4447 pub object_key : CompactString ,
45-
48+
4649 /// Optional factory key for object creation
4750 /// If not specified, object_key is used for factory lookup
4851 #[ serde( skip_serializing_if = "Option::is_none" , default ) ]
4952 pub factory_key : Option < CompactString > ,
50-
53+
5154 /// Format string to generate the value
5255 /// Supports Envoy substitution format strings like:
5356 /// - %REQ(:authority)% - Request header
5457 /// - %DOWNSTREAM_REMOTE_ADDRESS% - Client IP
5558 /// - %UPSTREAM_HOST% - Selected upstream
5659 pub format_string : FormatString ,
57-
60+
5861 /// Make this value read-only (cannot be overridden by other filters)
5962 #[ serde( skip_serializing_if = "std::ops::Not::not" , default ) ]
6063 pub read_only : bool ,
61-
64+
6265 /// Share with upstream internal connections
6366 #[ serde( skip_serializing_if = "is_default" , default ) ]
6467 pub shared_with_upstream : SharedWithUpstream ,
65-
68+
6669 /// Skip setting the value if it evaluates to empty string
6770 #[ serde( skip_serializing_if = "std::ops::Not::not" , default ) ]
6871 pub skip_if_empty : bool ,
@@ -82,13 +85,12 @@ pub enum SharedWithUpstream {
8285}
8386
8487/// Format string for generating filter state values
85- #[ derive( Debug , Clone , Deserialize , Serialize , PartialEq , Eq ) ]
86- #[ serde( untagged) ]
88+ #[ derive( Debug , Clone , Serialize , PartialEq , Eq ) ]
8789pub enum FormatString {
8890 /// Plain text format string with command operators
8991 /// Example: "%REQ(:authority)%"
9092 Text ( CompactString ) ,
91-
93+
9294 /// Structured format (JSON, etc.) - future extension
9395 Structured {
9496 format : CompactString ,
@@ -97,64 +99,100 @@ pub enum FormatString {
9799 } ,
98100}
99101
102+ impl < ' de > Deserialize < ' de > for FormatString {
103+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
104+ where
105+ D : serde:: Deserializer < ' de > ,
106+ {
107+ // Deserialize into a serde_json::Value first so we can support multiple input shapes
108+ let v = JsonValue :: deserialize ( deserializer) ?;
109+
110+ match v {
111+ JsonValue :: String ( s) => Ok ( FormatString :: Text ( CompactString :: from ( s) ) ) ,
112+ JsonValue :: Object ( mut map) => {
113+ // Envoy typed struct may use nested `text_format_source: { inline_string: "..." }`
114+ if let Some ( tf_source) = map. remove ( "text_format_source" ) {
115+ if let Some ( inline) = tf_source. get ( "inline_string" ) {
116+ if let Some ( s) = inline. as_str ( ) {
117+ return Ok ( FormatString :: Text ( CompactString :: from ( s) ) ) ;
118+ }
119+ }
120+ // Inline bytes or other specifiers not supported here
121+ }
122+
123+ // Also accept a direct `text_format` field
124+ if let Some ( text_fmt) = map. remove ( "text_format" ) {
125+ if let Some ( s) = text_fmt. as_str ( ) {
126+ return Ok ( FormatString :: Text ( CompactString :: from ( s) ) ) ;
127+ }
128+ }
129+
130+ // Structured form: look for `format` key
131+ if let Some ( format_field) = map. remove ( "format" ) {
132+ if let Some ( s) = format_field. as_str ( ) {
133+ let json_format = map. remove ( "json_format" ) ;
134+ return Ok ( FormatString :: Structured { format : CompactString :: from ( s) , json_format } ) ;
135+ }
136+ }
137+
138+ Err ( serde:: de:: Error :: custom ( "unsupported FormatString representation" ) )
139+ } ,
140+ other => Err ( serde:: de:: Error :: custom ( format ! ( "unexpected FormatString type: {:?}" , other) ) ) ,
141+ }
142+ }
143+ }
144+
100145#[ cfg( feature = "envoy-conversions" ) ]
101146mod envoy_conversions {
102147 use super :: * ;
103148 use crate :: config:: common:: * ;
104- use orion_data_plane_api:: envoy_data_plane_api:: {
105- envoy:: {
106- config:: core:: v3:: SubstitutionFormatString as EnvoySubstitutionFormatString ,
107- extensions:: filters:: {
108- common:: set_filter_state:: v3:: {
109- FilterStateValue as EnvoyFilterStateValue ,
110- filter_state_value:: {
111- Key as EnvoyKey ,
112- Value as EnvoyValue ,
113- SharedWithUpstream as EnvoySharedWithUpstream
114- } ,
149+ use orion_data_plane_api:: envoy_data_plane_api:: envoy:: {
150+ config:: core:: v3:: SubstitutionFormatString as EnvoySubstitutionFormatString ,
151+ extensions:: filters:: {
152+ common:: set_filter_state:: v3:: {
153+ filter_state_value:: {
154+ Key as EnvoyKey , SharedWithUpstream as EnvoySharedWithUpstream , Value as EnvoyValue ,
115155 } ,
116- http :: set_filter_state :: v3 :: Config as EnvoySetFilterStateConfig ,
156+ FilterStateValue as EnvoyFilterStateValue ,
117157 } ,
158+ http:: set_filter_state:: v3:: Config as EnvoySetFilterStateConfig ,
118159 } ,
119160 } ;
120-
161+
121162 impl TryFrom < EnvoySetFilterStateConfig > for SetFilterState {
122163 type Error = GenericError ;
123-
164+
124165 fn try_from ( envoy : EnvoySetFilterStateConfig ) -> Result < Self , Self :: Error > {
125- let on_request_headers = envoy. on_request_headers
166+ let on_request_headers = envoy
167+ . on_request_headers
126168 . into_iter ( )
127169 . map ( FilterStateValue :: try_from)
128170 . collect :: < Result < Vec < _ > , _ > > ( )
129171 . with_node ( "on_request_headers" ) ?;
130-
172+
131173 Ok ( Self { on_request_headers } )
132174 }
133175 }
134-
176+
135177 impl TryFrom < EnvoyFilterStateValue > for FilterStateValue {
136178 type Error = GenericError ;
137-
179+
138180 fn try_from ( envoy : EnvoyFilterStateValue ) -> Result < Self , Self :: Error > {
139181 let object_key = match envoy. key {
140182 Some ( EnvoyKey :: ObjectKey ( key) ) => CompactString :: from ( key) ,
141183 None => return Err ( GenericError :: from_msg ( "missing object_key in FilterStateValue" ) ) ,
142184 } ;
143-
144- let factory_key = if envoy. factory_key . is_empty ( ) {
145- None
146- } else {
147- Some ( envoy. factory_key . into ( ) )
148- } ;
149-
185+
186+ let factory_key = ( !envoy. factory_key . is_empty ( ) ) . then ( || envoy. factory_key . into ( ) ) ;
187+
150188 let format_string = match envoy. value {
151189 Some ( EnvoyValue :: FormatString ( fs) ) => FormatString :: try_from ( fs) . with_node ( "format_string" ) ?,
152190 None => return Err ( GenericError :: from_msg ( "missing format_string in FilterStateValue" ) ) ,
153191 } ;
154-
155- let shared_with_upstream = SharedWithUpstream :: try_from ( envoy . shared_with_upstream )
156- . with_node ( "shared_with_upstream" ) ?;
157-
192+
193+ let shared_with_upstream =
194+ SharedWithUpstream :: try_from ( envoy . shared_with_upstream ) . with_node ( "shared_with_upstream" ) ?;
195+
158196 Ok ( Self {
159197 object_key,
160198 factory_key,
@@ -165,34 +203,31 @@ mod envoy_conversions {
165203 } )
166204 }
167205 }
168-
206+
169207 impl TryFrom < EnvoySubstitutionFormatString > for FormatString {
170208 type Error = GenericError ;
171-
209+
172210 fn try_from ( envoy : EnvoySubstitutionFormatString ) -> Result < Self , Self :: Error > {
173211 use orion_data_plane_api:: envoy_data_plane_api:: envoy:: config:: core:: v3:: {
174- substitution_format_string:: Format ,
175- data_source:: Specifier ,
212+ data_source:: Specifier , substitution_format_string:: Format ,
176213 } ;
177-
214+
178215 match envoy. format {
179216 Some ( Format :: TextFormat ( text) ) => Ok ( FormatString :: Text ( text. into ( ) ) ) ,
180- Some ( Format :: TextFormatSource ( source) ) => {
181- match source. specifier {
182- Some ( Specifier :: InlineString ( s) ) => Ok ( FormatString :: Text ( s. into ( ) ) ) ,
183- Some ( Specifier :: InlineBytes ( b) ) => {
184- let s = String :: from_utf8 ( b)
185- . map_err ( |e| GenericError :: from_msg ( format ! ( "Invalid UTF-8 in format string: {}" , e) ) ) ?;
186- Ok ( FormatString :: Text ( s. into ( ) ) )
187- } ,
188- Some ( Specifier :: Filename ( _) ) => {
189- Err ( GenericError :: unsupported_variant ( "filename format strings not supported" ) )
190- } ,
191- Some ( Specifier :: EnvironmentVariable ( _) ) => {
192- Err ( GenericError :: unsupported_variant ( "environment variable format strings not supported" ) )
193- } ,
194- None => Err ( GenericError :: from_msg ( "missing format string specifier" ) ) ,
195- }
217+ Some ( Format :: TextFormatSource ( source) ) => match source. specifier {
218+ Some ( Specifier :: InlineString ( s) ) => Ok ( FormatString :: Text ( s. into ( ) ) ) ,
219+ Some ( Specifier :: InlineBytes ( b) ) => {
220+ let s = String :: from_utf8 ( b)
221+ . map_err ( |e| GenericError :: from_msg ( format ! ( "Invalid UTF-8 in format string: {}" , e) ) ) ?;
222+ Ok ( FormatString :: Text ( s. into ( ) ) )
223+ } ,
224+ Some ( Specifier :: Filename ( _) ) => {
225+ Err ( GenericError :: unsupported_variant ( "filename format strings not supported" ) )
226+ } ,
227+ Some ( Specifier :: EnvironmentVariable ( _) ) => {
228+ Err ( GenericError :: unsupported_variant ( "environment variable format strings not supported" ) )
229+ } ,
230+ None => Err ( GenericError :: from_msg ( "missing format string specifier" ) ) ,
196231 } ,
197232 Some ( Format :: JsonFormat ( _) ) => {
198233 // JSON format not yet supported - would need structured logging
@@ -204,7 +239,7 @@ mod envoy_conversions {
204239 }
205240 impl TryFrom < i32 > for SharedWithUpstream {
206241 type Error = GenericError ;
207-
242+
208243 fn try_from ( value : i32 ) -> Result < Self , Self :: Error > {
209244 match EnvoySharedWithUpstream :: try_from ( value) {
210245 Ok ( EnvoySharedWithUpstream :: None ) => Ok ( Self :: None ) ,
@@ -216,6 +251,15 @@ mod envoy_conversions {
216251 }
217252}
218253
254+ impl TypedStructFilter for SetFilterState {
255+ const TYPE_URL : & ' static str = "type.googleapis.com/envoy.extensions.filters.http.set_filter_state.v3.Config" ;
256+
257+ fn from_json_value ( value : JsonValue ) -> Result < Self , GenericError > {
258+ serde_json:: from_value ( value)
259+ . map_err ( |e| GenericError :: from_msg_with_cause ( "Failed to deserialize SetFilterState from JSON" , e) )
260+ }
261+ }
262+
219263#[ cfg( test) ]
220264mod tests {
221265 use super :: * ;
0 commit comments