Skip to main content

src/aws/internal/codec/json_float.gleam

//// awsJson special-float helpers.
////
//// The AWS JSON protocols (`awsJson1_0`, `awsJson1_1`, plus other
//// JSON-family wire formats) encode IEEE 754 specials as JSON strings:
//// `"NaN"`, `"Infinity"`, `"-Infinity"`. Plain JSON numbers cover
//// every finite float.
////
//// **Why a wrapper type instead of raw `Float`:** Erlang's `float()`
//// type cannot hold NaN or Infinity. `0.0/0.0` raises `badarith`;
//// `binary_to_term` rejects the IEEE 754 NaN bit pattern with
//// `badarg`; `<<F/float>>` bit-syntax matching refuses to bind such
//// values. So a faithful representation of an awsJson Float field on
//// the BEAM target requires an explicit sum type — there's no way to
//// pack `NaN` into an Erlang float. Rust's `f64` happens to support
//// these natively, which is why aws-sdk-rust gets away with `Option<f64>`.
////
//// Matches `aws-sdk-rust`'s `aws-smithy-types::Number::SpecialFloat`
//// — same three string spellings, same case-sensitivity.

import gleam/dynamic/decode.{type Decoder}
import gleam/json.{type Json}

/// Tagged float that can faithfully round-trip every IEEE 754 value
/// reachable from JSON in the awsJson protocols.
///
/// Generated code uses this for any field whose Smithy shape is
/// `smithy.api#Float` or `smithy.api#Double`. User construction:
///
///   FloatValue(1.5)
///   NaN
///   PosInfinity
///   NegInfinity
pub type SmithyFloat {
  FloatValue(Float)
  NaN
  PosInfinity
  NegInfinity
}

/// Encode a `SmithyFloat` as a JSON value. Finite values become JSON
/// numbers; the three IEEE 754 specials become JSON strings, matching
/// the awsJson wire spec.
pub fn encode(v: SmithyFloat) -> Json {
  case v {
    FloatValue(f) -> json.float(f)
    NaN -> json.string("NaN")
    PosInfinity -> json.string("Infinity")
    NegInfinity -> json.string("-Infinity")
  }
}

/// Decoder for a `SmithyFloat`. Accepts plain JSON numbers and the
/// three special-float strings.
pub fn decoder() -> Decoder(SmithyFloat) {
  decode.one_of(decode.map(decode.float, FloatValue), [from_string()])
}

fn from_string() -> Decoder(SmithyFloat) {
  decode.then(decode.string, fn(s) {
    case s {
      "NaN" -> decode.success(NaN)
      "Infinity" -> decode.success(PosInfinity)
      "-Infinity" -> decode.success(NegInfinity)
      _ -> decode.failure(NaN, "expected NaN / Infinity / -Infinity")
    }
  })
}