@@ -10,6 +10,7 @@ use crate::types::errors::runtime_error::RuntimeError;
1010use crate :: types:: signal:: Signal ;
1111use crate :: value:: number_to_string;
1212use base64:: Engine ;
13+ use lupus:: { DecodeContext , EncodeContext , Engine as ConversionEngine , Format } ;
1314use serde_json:: Value as JsonValue ;
1415use std:: collections:: HashMap ;
1516use std:: io:: Read ;
@@ -151,7 +152,7 @@ fn send_request(
151152 http_auth: Value ,
152153 http_auth_value: Value ,
153154 http_auth_place: Value ,
154- _http_schema : Value ,
155+ http_schema : String ,
155156 payload: Value ,
156157 headers: Value ,
157158 ) ;
@@ -179,22 +180,12 @@ fn send_request(
179180 return fail ( "InvalidArgumentRuntimeError" , message) ;
180181 }
181182
182- let request_content_type = content_type_header_value ( & headers) ;
183- let ( request_body, default_content_type) =
184- match encode_request_payload ( & payload, request_content_type. as_deref ( ) ) {
185- Ok ( result) => result,
186- Err ( message) => return fail ( "InvalidArgumentRuntimeError" , message) ,
187- } ;
183+ let request_body = match encode_request_payload ( & payload, & http_schema) {
184+ Ok ( result) => result,
185+ Err ( message) => return fail ( "InvalidArgumentRuntimeError" , message) ,
186+ } ;
188187
189- if let Some ( default_content_type) = default_content_type
190- && request_content_type. is_none ( )
191- {
192- insert_header (
193- & mut headers,
194- "content-type" ,
195- default_content_type. to_string ( ) ,
196- ) ;
197- }
188+ insert_header ( & mut headers, "content-type" , http_schema) ;
198189
199190 let http_method = match http:: Method :: from_bytes ( http_method. as_bytes ( ) ) {
200191 Ok ( value) => value,
@@ -419,16 +410,6 @@ fn value_to_string(value: &Value) -> Result<String, String> {
419410 }
420411}
421412
422- fn content_type_header_value ( headers : & HashMap < String , String > ) -> Option < String > {
423- headers. iter ( ) . find_map ( |( name, value) | {
424- if name. eq_ignore_ascii_case ( "content-type" ) {
425- Some ( value. clone ( ) )
426- } else {
427- None
428- }
429- } )
430- }
431-
432413fn normalize_content_type ( content_type : & str ) -> String {
433414 content_type
434415 . split ( ';' )
@@ -438,54 +419,52 @@ fn normalize_content_type(content_type: &str) -> String {
438419 . to_ascii_lowercase ( )
439420}
440421
441- #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
442- enum RequestBodyEncoding {
443- Json ,
444- Text ,
445- }
446-
447- fn resolve_request_body_encoding (
448- payload : & Value ,
449- request_content_type : Option < & str > ,
450- ) -> Result < Option < RequestBodyEncoding > , String > {
451- if let Some ( content_type) = request_content_type {
452- let normalized = normalize_content_type ( content_type) ;
453- if content_type_is_json ( & normalized) {
454- return Ok ( Some ( RequestBodyEncoding :: Json ) ) ;
455- }
456- return Ok ( Some ( RequestBodyEncoding :: Text ) ) ;
422+ fn encode_request_payload ( payload : & Value , content_type : & str ) -> Result < Option < Vec < u8 > > , String > {
423+ if matches ! ( payload. kind. as_ref( ) , Some ( Kind :: NullValue ( _) ) | None ) {
424+ return Ok ( None ) ;
457425 }
458426
459- match payload. kind . as_ref ( ) {
460- Some ( Kind :: NullValue ( _) ) | None => Ok ( None ) ,
461- Some ( Kind :: StringValue ( _) ) => Ok ( Some ( RequestBodyEncoding :: Text ) ) ,
462- _ => Ok ( Some ( RequestBodyEncoding :: Json ) ) ,
463- }
427+ let format = format_for_content_type ( content_type) ?;
428+ let protobuf = serde_json:: to_vec ( payload)
429+ . map_err ( |err| format ! ( "Unable to serialize protobuf request payload: {err}" ) ) ?;
430+ let engine = ConversionEngine :: with_default_codecs ( ) ;
431+ let body = engine
432+ . convert (
433+ & protobuf,
434+ Format :: Protobuf ,
435+ format,
436+ & DecodeContext ,
437+ & EncodeContext :: default ( ) ,
438+ )
439+ . map_err ( |err| {
440+ format ! (
441+ "Unable to convert request payload to '{}': {err}" ,
442+ content_type
443+ )
444+ } ) ?;
445+ Ok ( Some ( body) )
464446}
465447
466- fn encode_request_payload (
467- payload : & Value ,
468- request_content_type : Option < & str > ,
469- ) -> Result < ( Option < Vec < u8 > > , Option < & ' static str > ) , String > {
470- let Some ( encoding) = resolve_request_body_encoding ( payload, request_content_type) ? else {
471- return Ok ( ( None , None ) ) ;
472- } ;
473-
474- match encoding {
475- RequestBodyEncoding :: Json => {
476- let json = to_json_value ( payload. clone ( ) ) ;
477- let body = serde_json:: to_vec ( & json)
478- . map_err ( |err| format ! ( "Unable to serialize request payload: {}" , err) ) ?;
479- Ok ( ( Some ( body) , Some ( "application/json" ) ) )
448+ fn format_for_content_type ( content_type : & str ) -> Result < Format , String > {
449+ let normalized = normalize_content_type ( content_type) ;
450+ let format = match normalized. as_str ( ) {
451+ "application/json" | "text/json" => Format :: Json ,
452+ value if value. ends_with ( "+json" ) => Format :: Json ,
453+ "application/xhtml+xml" => Format :: Html ,
454+ "application/xml" | "text/xml" => Format :: Xml ,
455+ value if value. ends_with ( "+xml" ) => Format :: Xml ,
456+ "text/html" => Format :: Html ,
457+ "text/plain" => Format :: Text ,
458+ "text/csv" | "application/csv" => Format :: Csv ,
459+ "application/x-www-form-urlencoded" => Format :: HttpForm ,
460+ _ => {
461+ return Err ( format ! (
462+ "Unsupported content-type '{}' for http::request::send" ,
463+ content_type
464+ ) ) ;
480465 }
481- RequestBodyEncoding :: Text => match payload. kind . as_ref ( ) {
482- Some ( Kind :: NullValue ( _) ) | None => Ok ( ( None , Some ( "text/plain" ) ) ) ,
483- Some ( Kind :: StringValue ( body) ) => {
484- Ok ( ( Some ( body. as_bytes ( ) . to_vec ( ) ) , Some ( "text/plain" ) ) )
485- }
486- _ => Err ( "Payload must be StringValue when content-type is not JSON" . to_string ( ) ) ,
487- } ,
488- }
466+ } ;
467+ Ok ( format)
489468}
490469
491470fn decode_headers ( response : & http:: Response < Body > ) -> Struct {
@@ -555,88 +534,163 @@ mod tests {
555534 value. to_string ( ) . to_value ( )
556535 }
557536
558- #[ test]
559- fn encode_request_payload_serializes_non_string_values_to_json ( ) {
560- let payload = Value {
561- kind : Some ( Kind :: StructValue ( Struct {
562- fields : HashMap :: from ( [ (
563- "ok" . to_string ( ) ,
564- Value {
565- kind : Some ( Kind :: BoolValue ( true ) ) ,
566- } ,
567- ) ] ) ,
568- } ) ) ,
569- } ;
537+ fn encoded_body ( payload : & Value , content_type : & str ) -> Vec < u8 > {
538+ encode_request_payload ( payload, content_type)
539+ . unwrap_or_else ( |err| panic ! ( "unable to encode {content_type} payload: {err}" ) )
540+ . unwrap_or_default ( )
541+ }
570542
571- let ( body, content_type) = match encode_request_payload ( & payload, None ) {
572- Ok ( result) => result,
573- Err ( err) => panic ! ( "unexpected error: {}" , err) ,
574- } ;
543+ #[ test]
544+ fn encode_request_payload_serializes_every_non_null_value_kind_to_json ( ) {
545+ let cases = [
546+ ( serde_json:: json!( true ) , "true" ) ,
547+ ( serde_json:: json!( 42 ) , "42" ) ,
548+ ( serde_json:: json!( 1.5 ) , "1.5" ) ,
549+ ( serde_json:: json!( "hello" ) , r#""hello""# ) ,
550+ ( serde_json:: json!( [ "hello" , 42 ] ) , r#"["hello",42]"# ) ,
551+ (
552+ serde_json:: json!( { "active" : true , "name" : "Tom" } ) ,
553+ r#"{"active":true,"name":"Tom"}"# ,
554+ ) ,
555+ ] ;
575556
576- assert_eq ! ( content_type, Some ( "application/json" ) ) ;
577- let body = body. unwrap_or_default ( ) ;
578- let text = match String :: from_utf8 ( body) {
579- Ok ( text) => text,
580- Err ( err) => panic ! ( "payload was not valid utf8: {}" , err) ,
581- } ;
582- let decoded = serde_json:: from_str :: < JsonValue > ( & text) . unwrap_or ( JsonValue :: Null ) ;
583- let JsonValue :: Object ( map) = decoded else {
584- panic ! ( "expected encoded payload to be json object" ) ;
585- } ;
586- let Some ( JsonValue :: Bool ( ok) ) = map. get ( "ok" ) else {
587- panic ! ( "missing ok field in json payload" ) ;
588- } ;
589- assert ! ( * ok) ;
557+ for ( input, expected) in cases {
558+ let payload = from_json_value ( input) ;
559+ assert_eq ! (
560+ encoded_body( & payload, "application/json" ) ,
561+ expected. as_bytes( )
562+ ) ;
563+ }
564+ }
590565
591- let ( empty_body, empty_content_type) = encode_request_payload (
592- & Value {
566+ #[ test]
567+ fn encode_request_payload_omits_null_and_missing_values ( ) {
568+ for payload in [
569+ Value {
593570 kind : Some ( Kind :: NullValue ( 0 ) ) ,
594571 } ,
595- None ,
596- )
597- . unwrap_or ( ( Some ( vec ! [ 1 ] ) , Some ( "application/json" ) ) ) ;
598- assert_eq ! ( empty_content_type, None ) ;
599- assert ! ( empty_body. is_none( ) ) ;
572+ Value { kind : None } ,
573+ ] {
574+ assert_eq ! (
575+ encode_request_payload( & payload, "application/json" ) . unwrap_or( Some ( vec![ 1 ] ) ) ,
576+ None
577+ ) ;
578+ }
600579 }
601580
602581 #[ test]
603- fn encode_request_payload_uses_text_for_non_json_content_type ( ) {
604- let ( text_body, text_content_type) =
605- encode_request_payload ( & string_value ( "hello" ) , Some ( "text/plain; charset=utf-8" ) )
606- . unwrap_or ( ( None , None ) ) ;
582+ fn encode_request_payload_does_not_detect_formats_inside_strings ( ) {
583+ let text_body = encode_request_payload ( & string_value ( "hello" ) , "text/plain; charset=utf-8" )
584+ . unwrap_or ( None ) ;
607585
608- assert_eq ! ( text_content_type, Some ( "text/plain" ) ) ;
609586 let body = text_body. unwrap_or_default ( ) ;
610587 assert_eq ! ( body, b"hello" ) ;
611588
612- let ( xml_body, xml_content_type) =
613- encode_request_payload ( & string_value ( "<ok />" ) , Some ( "application/xml" ) )
614- . unwrap_or ( ( None , None ) ) ;
615- assert_eq ! ( xml_content_type, Some ( "text/plain" ) ) ;
616- assert_eq ! ( xml_body. unwrap_or_default( ) , b"<ok />" ) ;
589+ let json_body =
590+ encode_request_payload ( & string_value ( r#"{"user":"tom"}"# ) , "application/json" )
591+ . unwrap_or ( None ) ;
592+ assert_eq ! ( json_body. unwrap_or_default( ) , br#""{\"user\":\"tom\"}""# ) ;
617593
618- let ( empty_body, empty_content_type) = encode_request_payload (
619- & Value {
620- kind : Some ( Kind :: NullValue ( 0 ) ) ,
621- } ,
622- Some ( "application/octet-stream" ) ,
623- )
624- . unwrap_or ( ( Some ( vec ! [ 1 ] ) , None ) ) ;
625- assert_eq ! ( empty_content_type, Some ( "text/plain" ) ) ;
626- assert ! ( empty_body. is_none( ) ) ;
627-
628- let err = encode_request_payload (
629- & Value {
630- kind : Some ( Kind :: StructValue ( Struct {
631- fields : HashMap :: from ( [ ( "a" . to_string ( ) , 1i64 . to_value ( ) ) ] ) ,
632- } ) ) ,
633- } ,
634- Some ( "text/plain" ) ,
594+ let xml_body =
595+ encode_request_payload ( & string_value ( "<ok />" ) , "application/xml" ) . unwrap_or ( None ) ;
596+ assert_eq ! ( xml_body. unwrap_or_default( ) , b"<data><ok /></data>" ) ;
597+ }
598+
599+ #[ test]
600+ fn encode_request_payload_supports_every_http_target_format ( ) {
601+ let object = from_json_value ( serde_json:: json!( {
602+ "user" : {
603+ "name" : "Tom"
604+ }
605+ } ) ) ;
606+ assert_eq ! (
607+ encoded_body( & object, "application/json" ) ,
608+ br#"{"user":{"name":"Tom"}}"#
609+ ) ;
610+ assert_eq ! (
611+ encoded_body( & object, "application/xml" ) ,
612+ b"<user><name>Tom</name></user>"
613+ ) ;
614+ assert_eq ! (
615+ encoded_body( & object, "text/html" ) ,
616+ b"<user><name>Tom</name></user>"
617+ ) ;
618+ assert_eq ! (
619+ encoded_body( & string_value( "hello world" ) , "text/plain" ) ,
620+ b"hello world"
635621 ) ;
622+
623+ let rows = from_json_value ( serde_json:: json!( [
624+ { "name" : "Tom" , "role" : "admin" } ,
625+ { "name" : "Ada" , "role" : "user" }
626+ ] ) ) ;
627+ assert_eq ! (
628+ encoded_body( & rows, "text/csv" ) ,
629+ b"name,role\n Tom,admin\n Ada,user\n "
630+ ) ;
631+
632+ let form = from_json_value ( serde_json:: json!( {
633+ "email" : "tom@example.com" ,
634+ "name" : "Tom Doe"
635+ } ) ) ;
636+ assert_eq ! (
637+ encoded_body( & form, "application/x-www-form-urlencoded" ) ,
638+ b"email=tom%40example.com&name=Tom+Doe"
639+ ) ;
640+ }
641+
642+ #[ test]
643+ fn format_for_content_type_supports_parameters_and_standard_aliases ( ) {
644+ let cases = [
645+ ( "application/json; charset=utf-8" , Format :: Json ) ,
646+ ( "text/json" , Format :: Json ) ,
647+ ( "application/problem+json" , Format :: Json ) ,
648+ ( "application/xml; charset=utf-8" , Format :: Xml ) ,
649+ ( "text/xml" , Format :: Xml ) ,
650+ ( "application/atom+xml" , Format :: Xml ) ,
651+ ( "text/html" , Format :: Html ) ,
652+ ( "application/xhtml+xml" , Format :: Html ) ,
653+ ( "text/plain; charset=utf-8" , Format :: Text ) ,
654+ ( "text/csv" , Format :: Csv ) ,
655+ ( "application/csv" , Format :: Csv ) ,
656+ ( "application/x-www-form-urlencoded" , Format :: HttpForm ) ,
657+ ] ;
658+
659+ for ( content_type, expected) in cases {
660+ assert_eq ! (
661+ format_for_content_type( content_type) ,
662+ Ok ( expected) ,
663+ "unexpected mapping for {content_type}"
664+ ) ;
665+ }
666+ }
667+
668+ #[ test]
669+ fn encode_request_payload_rejects_unsupported_content_types ( ) {
670+ let err = encode_request_payload ( & string_value ( "hello" ) , "application/octet-stream" ) ;
636671 let Err ( err) = err else {
637- panic ! ( "expected text/plain payload validation error" ) ;
672+ panic ! ( "expected unsupported content-type error" ) ;
638673 } ;
639- assert ! ( err. contains( "Payload must be StringValue" ) ) ;
674+ assert ! ( err. contains( "Unsupported content-type" ) ) ;
675+ }
676+
677+ #[ test]
678+ fn encode_request_payload_propagates_information_loss_errors ( ) {
679+ let invalid_csv = from_json_value ( serde_json:: json!( {
680+ "name" : "Tom"
681+ } ) ) ;
682+ let csv_error = encode_request_payload ( & invalid_csv, "text/csv" )
683+ . expect_err ( "object should not encode as CSV rows" ) ;
684+ assert ! ( csv_error. contains( "conversion would lose information" ) ) ;
685+ assert ! ( csv_error. contains( "top-level array" ) ) ;
686+
687+ let invalid_form = from_json_value ( serde_json:: json!( {
688+ "active" : true
689+ } ) ) ;
690+ let form_error = encode_request_payload ( & invalid_form, "application/x-www-form-urlencoded" )
691+ . expect_err ( "typed form value should not be coerced to a string" ) ;
692+ assert ! ( form_error. contains( "conversion would lose information" ) ) ;
693+ assert ! ( form_error. contains( "form fields must be strings" ) ) ;
640694 }
641695
642696 #[ test]
@@ -864,7 +918,10 @@ mod tests {
864918 } ) ) ,
865919 } ;
866920 let request_headers = Struct {
867- fields : HashMap :: from ( [ ( "x-bool" . to_string ( ) , true . to_value ( ) ) ] ) ,
921+ fields : HashMap :: from ( [
922+ ( "x-bool" . to_string ( ) , true . to_value ( ) ) ,
923+ ( "Content-Type" . to_string ( ) , string_value ( "text/plain" ) ) ,
924+ ] ) ,
868925 } ;
869926 let args = vec ! [
870927 Argument :: Eval ( string_value( "POST" ) ) ,
0 commit comments