Module: Ruby::Rego::Builtins::Units
- Extended by:
- RegistryHelpers
- Defined in:
- lib/ruby/rego/builtins/units.rb
Overview
Resource-quantity parsers (units.parse, units.parse_bytes), matching OPA. units.parse
returns an integer or a value rounded to 10 decimals; units.parse_bytes lowercases its
input, multiplies, and truncates toward zero to an integer. A non-string, an empty
amount, an embedded space, an unrecognised unit, an unparseable amount, or a scientific
exponent of more than MAX_EXPONENT_DIGITS digits yields undefined.
Both are byte-for-byte faithful to OPA, but via DIFFERENT arithmetic, matching OPA’s two code
paths: units.parse uses exact rational arithmetic (OPA’s big.Rat), while units.parse_bytes
computes in a 64-bit big.Float (OPA’s big.Float: SetString → Mul → truncate toward zero, via
Number.prec64_multiply_truncate). The big.Float path rounds, so a fractional byte amount can
differ from the exact-rational value (0.001mb → 999; 9999999999.99999999995 → 10000000000), as
OPA does. Both share the same amount grammar (AMOUNT_RE) and ASCII/length DoS guards.
Constant Summary collapse
- MAX_EXPONENT_DIGITS =
OPA’s maxExponentDigits: a scientific exponent longer than this is undefined.
6- ROUND_DECIMALS =
OPA renders a non-integer units.parse result via big.Rat.FloatString(10): exactly 10 decimal places, rounded half-away-from-zero, trailing zeros kept (see float_string).
10- DECIMAL_SCALE =
10**ROUND_DECIMALS
- MAX_SOURCE =
Upper bound on the operand length (OPA has none — it relies on Go’s runtime). Guards the pure-Ruby evaluator against a huge literal numeric string allocating a giant bignum; an over-long operand yields undefined. (The 6-digit exponent cap already bounds scientific-notation blow-up.)
1_000_000- NUMERIC =
Characters that continue the numeric prefix (an
e/Eexponent is handled apart). "0123456789.+-"- DIGIT =
/[0-9]/- SIGN =
%w[+ -].freeze
- AMOUNT_RE =
Go’s big.Rat.SetString decimal-float grammar (the subset reachable after number_boundary): an optional sign, a mantissa with at least one digit (so a bare “.” is rejected), and an optional exponent that requires at least one digit (so a dangling “1e+” is rejected). Ruby’s Rational is both more lenient (accepts “.” and “1e+”) and stricter (“5.e3”), so the amount is validated against this before being handed to Rational. Anchored to the whole amount.
/\A[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\z/- PARSE_UNITS =
units.parse multipliers. Note the m/M asymmetry: lowercase
mis milli, uppercaseMis mega; every other first letter is case-insensitive. Keys are the unit with its first character kept and the remainder lowercased, sokI/kicollide whileKistays distinct fromki(hence both appear below). Milli uses the IEEE-754 double of 0.001 (as OPA’s big.Rat.SetFloat64 does), so e.g.1000myields1.0, not1. { "" => 1, "m" => 0.001.to_r, "k" => 1_000, "K" => 1_000, "M" => 1_000_000, "g" => 10**9, "G" => 10**9, "t" => 10**12, "T" => 10**12, "p" => 10**15, "P" => 10**15, "e" => 10**18, "E" => 10**18, "ki" => 1024, "Ki" => 1024, "mi" => 1024**2, "Mi" => 1024**2, "gi" => 1024**3, "Gi" => 1024**3, "ti" => 1024**4, "Ti" => 1024**4, "pi" => 1024**5, "Pi" => 1024**5, "ei" => 1024**6, "Ei" => 1024**6 }.freeze
- PARSE_BYTES_UNITS =
units.parse_bytes multipliers (the whole input is lowercased first, so keys are too). Both the
b-suffixed and bare forms are accepted; a barebis NOT a unit. { "" => 1, "kb" => 1000, "k" => 1000, "kib" => 1024, "ki" => 1024, "mb" => 10**6, "m" => 10**6, "mib" => 1024**2, "mi" => 1024**2, "gb" => 10**9, "g" => 10**9, "gib" => 1024**3, "gi" => 1024**3, "tb" => 10**12, "t" => 10**12, "tib" => 1024**4, "ti" => 1024**4, "pb" => 10**15, "p" => 10**15, "pib" => 1024**5, "pi" => 1024**5, "eb" => 10**18, "e" => 10**18, "eib" => 1024**6, "ei" => 1024**6 }.freeze
- UNITS_FUNCTIONS =
{ "units.parse" => { arity: 1, handler: :parse }, "units.parse_bytes" => { arity: 1, handler: :parse_bytes } }.freeze
Class Method Summary collapse
-
.parse(value) ⇒ Integer, Ruby::Rego::Number
Parses an SI/binary quantity (e.g. “10K”, “1.5Mi”, “10m”) to a number — exact rational arithmetic.
-
.parse_bytes(value) ⇒ Integer
Parses a byte quantity (e.g. “10KB”, “1.5GiB”) to an integer, truncating toward zero.
-
.register! ⇒ Ruby::Rego::Builtins::BuiltinRegistry
Methods included from RegistryHelpers
Class Method Details
.parse(value) ⇒ Integer, Ruby::Rego::Number
Parses an SI/binary quantity (e.g. “10K”, “1.5Mi”, “10m”) to a number — exact rational arithmetic. An integer-valued result is an exact Integer; a non-integer result is a precision-preserving Number rendered to 10 decimals the way OPA’s big.Rat does.
100 101 102 103 104 105 106 107 108 |
# File 'lib/ruby/rego/builtins/units.rb', line 100 def self.parse(value) amount, unit = split(string_arg(value, "units.parse"), "units.parse") multiplier = PARSE_UNITS[normalize_unit(unit)] || raise_unrecognized(unit, "units.parse") result = parse_amount(amount, "units.parse") * multiplier # @type var result: Rational # Number.literal (not from_numeric/from_binnum, which route through GoNumberFormat's shortest # form and would strip the trailing zeros) preserves the fixed 10-decimal text verbatim; its # exact value is re-derived lazily from that text. result.denominator == 1 ? result.numerator : Number.literal(float_string(result)) end |
.parse_bytes(value) ⇒ Integer
Parses a byte quantity (e.g. “10KB”, “1.5GiB”) to an integer, truncating toward zero.
139 140 141 142 143 144 145 146 147 148 |
# File 'lib/ruby/rego/builtins/units.rb', line 139 def self.parse_bytes(value) amount, unit = split(string_arg(value, "units.parse_bytes").downcase, "units.parse_bytes") multiplier = PARSE_BYTES_UNITS[unit] || raise_unrecognized(unit, "units.parse_bytes") # OPA computes the byte count in a 64-bit big.Float, not exact rational, so a fractional # amount can round (`0.001mb` -> 999, not 1000) — see Number.prec64_multiply_truncate. The # amount is bounded by MAX_SOURCE and MAX_EXPONENT_DIGITS to a magnitude far below the engine's # emax (2**30), so the product is always finite and `to_i` never raises on an infinity — which # would otherwise escape as a non-BuiltinArgumentError and abort the whole policy (a DoS). Number.prec64_multiply_truncate(parse_amount(amount, "units.parse_bytes"), multiplier) end |
.register! ⇒ Ruby::Rego::Builtins::BuiltinRegistry
86 87 88 89 90 |
# File 'lib/ruby/rego/builtins/units.rb', line 86 def self.register! registry = BuiltinRegistry.instance register_configured_functions(registry, UNITS_FUNCTIONS) registry end |