//// XML encoder helpers for restXml / awsQuery / ec2Query bodies. //// Minimum viable: element-per-member, text-content for primitives, //// proper escaping of `<>&"'`. Honours `@xmlName` overrides at the //// call site (the emitter passes the resolved element name). //// //// The XML response decoder lives in //// `src/aws/internal/codec/xml_decode.gleam` — kept separate because //// it pulls in xmerl via FFI, which is heavier than the pure-string //// encoder side. import gleam/bit_array import gleam/dict.{type Dict} import gleam/list import gleam/string /// `...` wrapper around inner content. pub fn element(name: String, inner: String) -> String { "<" <> name <> ">" <> inner <> " name <> ">" } /// `inner`. Attribute values are /// escaped the same way as text content; attribute names are passed /// verbatim and assumed to be safe (Smithy member names). pub fn element_with_attrs( name: String, attrs: List(#(String, String)), inner: String, ) -> String { let attr_str = case attrs { [] -> "" _ -> list.fold(attrs, "", fn(acc, p) { let #(k, v) = p acc <> " " <> k <> "=\"" <> escape_attr(v) <> "\"" }) } "<" <> name <> attr_str <> ">" <> inner <> " name <> ">" } /// `` — short form for empty elements. pub fn empty_element(name: String) -> String { "<" <> name <> "/>" } pub fn text(value: String) -> String { escape_text(value) } /// Escape `<`, `>`, `&` in element text content. pub fn escape_text(s: String) -> String { s |> string.replace("&", "&") |> string.replace("<", "<") |> string.replace(">", ">") } /// Escape `&`, `<`, `>`, `"`, and `'` in attribute values. AWS /// service XML is double-quoted, so `'` doesn't strictly need /// escaping, but the entity is harmless and keeps the encoder /// quote-style agnostic. pub fn escape_attr(s: String) -> String { s |> escape_text |> string.replace("\"", """) |> string.replace("'", "'") } /// Format helpers for primitive members. pub fn bool_text(b: Bool) -> String { case b { True -> "true" False -> "false" } } @external(erlang, "erlang", "integer_to_binary") pub fn int_text(n: Int) -> String @external(erlang, "aws_ffi", "float_short") pub fn float_text(f: Float) -> String /// Base64-encode a blob value for XML body inclusion. S3 uses base64 /// for binary fields inside XML bodies (Content-MD5, ChecksumSHA256). pub fn blob_text(b: BitArray) -> String { bit_array.base64_encode(b, True) } /// Build a list element with each entry as `value` /// children. The wrapper element comes from the list shape's name; the /// per-entry element name comes from the list member's `@xmlName` (the /// emitter passes both). pub fn list_element( wrapper: String, member_name: String, entries: List(String), ) -> String { let inner = list.fold(entries, "", fn(acc, entry_inner) { acc <> element(member_name, entry_inner) }) element(wrapper, inner) } /// Same as `list_element` but for `@xmlFlattened` lists — no /// wrapper element; entries become direct siblings. pub fn flat_list(member_name: String, entries: List(String)) -> String { list.fold(entries, "", fn(acc, entry_inner) { acc <> element(member_name, entry_inner) }) } /// Build a map element: each entry becomes `KV` /// inside a wrapper. Smithy default uses `entry` / `key` / `value` — /// `@xmlName` overrides land in the caller. pub fn map_element( wrapper: String, key_name: String, value_name: String, entries: Dict(String, String), ) -> String { element(wrapper, map_entries(key_name, value_name, entries)) } /// Just the `...` siblings of a map, no wrapper. /// Used for nested maps — the outer map's `` element wraps /// the inner map's entries directly. pub fn map_entries( key_name: String, value_name: String, entries: Dict(String, String), ) -> String { dict.fold(entries, "", fn(acc, k, v) { acc <> element( "entry", element(key_name, escape_text(k)) <> element(value_name, v), ) }) } /// `@xmlFlattened` map: each entry becomes a top-level /// `KV` block /// — no outer wrapper, no `` tag. The caller's containing /// element provides the sequencing. `value_name` is the key / value /// inner-element name (`"key"` / `"value"` by default; `@xmlName` /// overrides land in the caller). pub fn flat_map( member_name: String, key_name: String, value_name: String, entries: Dict(String, String), ) -> String { dict.fold(entries, "", fn(acc, k, v) { acc <> element( member_name, element(key_name, escape_text(k)) <> element(value_name, v), ) }) } /// `list_element` variant that carries member-level XML namespace /// attributes. `wrapper_attrs` go on the outer wrapping element; /// `member_attrs` go on every per-entry wrapping. Either list may /// be empty, in which case the corresponding wrapper renders /// without attributes — equivalent to `list_element` in that case. pub fn list_element_ns( wrapper: String, wrapper_attrs: List(#(String, String)), member_name: String, member_attrs: List(#(String, String)), entries: List(String), ) -> String { let inner = list.fold(entries, "", fn(acc, entry_inner) { acc <> element_with_attrs(member_name, member_attrs, entry_inner) }) element_with_attrs(wrapper, wrapper_attrs, inner) } /// `flat_list` variant with namespace attributes on every emitted /// member element. Empty `member_attrs` collapses to a bare element. pub fn flat_list_ns( member_name: String, member_attrs: List(#(String, String)), entries: List(String), ) -> String { list.fold(entries, "", fn(acc, entry_inner) { acc <> element_with_attrs(member_name, member_attrs, entry_inner) }) } /// `map_element` variant with namespace attributes on the outer /// wrapper plus the key / value wrappers. The intermediate /// `` wrapper is unattributed (Smithy doesn't expose a /// trait for it). pub fn map_element_ns( wrapper: String, wrapper_attrs: List(#(String, String)), key_name: String, key_attrs: List(#(String, String)), value_name: String, value_attrs: List(#(String, String)), entries: Dict(String, String), ) -> String { element_with_attrs( wrapper, wrapper_attrs, map_entries_ns(key_name, key_attrs, value_name, value_attrs, entries), ) } /// `map_entries` variant with namespace attributes on the key / /// value wrappers. Used for nested maps and as the body of /// `map_element_ns`. pub fn map_entries_ns( key_name: String, key_attrs: List(#(String, String)), value_name: String, value_attrs: List(#(String, String)), entries: Dict(String, String), ) -> String { dict.fold(entries, "", fn(acc, k, v) { acc <> element( "entry", element_with_attrs(key_name, key_attrs, escape_text(k)) <> element_with_attrs(value_name, value_attrs, v), ) }) } /// `flat_map` variant with namespace attributes on the repeated /// member wrappers and on the key / value wrappers inside. pub fn flat_map_ns( member_name: String, member_attrs: List(#(String, String)), key_name: String, key_attrs: List(#(String, String)), value_name: String, value_attrs: List(#(String, String)), entries: Dict(String, String), ) -> String { dict.fold(entries, "", fn(acc, k, v) { acc <> element_with_attrs( member_name, member_attrs, element_with_attrs(key_name, key_attrs, escape_text(k)) <> element_with_attrs(value_name, value_attrs, v), ) }) }