// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// UUID that complies with RFC 4122.
///
/// UUID objects are immutable, hashable and usable as mapping keys.
/// The Display of UUIDs is like `12345678-1234-1234-1234-123456789abc`;
/// UUIDs can be constructed using such strings, or 16-bytes strings with big-
/// endian byte order.
///
/// # Usage
///
/// ```moonbit check
/// test {
/// let u1 = @uuid.from_hex("ddf99703-742f-7505-4c54-df36a9c243fe")
/// let u2 = @uuid.from_bytes(
/// Bytes::from_array([
/// 221, 249, 151, 3, 116, 47, 117, 5, 76, 84, 223, 54, 169, 194, 67, 254,
/// ]),
/// )
/// assert_eq(u1, u2)
/// }
/// ```
///
/// Construct an RFC-4122-compliant v4 UUID:
///
/// ```moonbit check
/// test {
/// let _u = @uuid.from_hex("9558cfadb48547c70f9adf1f75cdef29").as_version(V4)
/// }
/// ```
///
struct UUID {
// XXX: replace with an 128-bit data structure when there is one
// use the derived hash
hi : Int64
lo : Int64
} derive(Eq, Compare)
///|
/// Constructs a UUID with the given big-endian bytes.
///
/// The produced UUID is not RFC-4122 compliant; see also `as_version()`.
///
pub fn from_bytes(bytes : Bytes) -> UUID raise Error {
if bytes.length() != 16 {
fail("requires 16 bytes")
}
// XXX: this is ideally Int64::from_bytes() (needs a cheap slicing method)
let hi = for rv = 0L, i = 0; i < 8; {
continue rv | (bytes[i].to_int64() << ((7 - i) * 8)), i + 1
} nobreak {
rv
}
let lo = for rv = 0L, i = 8; i < 16; {
continue rv | (bytes[i].to_int64() << ((15 - i) * 8)), i + 1
} nobreak {
rv
}
{ hi, lo }
}
///|
/// The UUID as a 16-byte string.
pub fn UUID::to_bytes(self : UUID) -> Bytes {
let result = FixedArray::make(16, b'\x00')
for i = 0; i < 8; i = i + 1 {
result[i] = (self.hi.reinterpret_as_uint64() >> ((7 - i) * 8)).to_byte()
}
for i = 8; i < 16; i = i + 1 {
result[i] = (self.lo.reinterpret_as_uint64() >> ((15 - i) * 8)).to_byte()
}
// move the result ownnership to Bytes
result.unsafe_reinterpret_as_bytes()
}
///|
test "bytes" {
let buf = Bytes::from_array(
[201, 130, 251, 104, 223, 80, 76, 161, 145, 248, 186, 162, 4, 9, 229, 185].map(
Int::to_byte,
),
)
inspect(
from_bytes(buf).to_bytes(),
content=(
#|b"\xc9\x82\xfbh\xdfPL\xa1\x91\xf8\xba\xa2\x04\x09\xe5\xb9"
),
)
assert_true(
try from_bytes(Bytes::new(15)) catch {
_ => true
} noraise {
_ => false
},
)
assert_true(
try from_bytes(Bytes::new(17)) catch {
_ => true
} noraise {
_ => false
},
)
}
///|
fn hex_char_to_int64(c : Int) -> Int64 raise Error {
let d = if '0'.to_int() <= c && c <= '9'.to_int() {
c - '0'
} else if 'a'.to_int() <= c && c <= 'f'.to_int() {
c - 'a' + 10
} else if 'A'.to_int() <= c && c <= 'F'.to_int() {
c - 'A' + 10
} else {
fail("invalid syntax")
}
d.to_int64()
}
///|
/// Constructs a UUID with the given hexadecimal string.
///
/// This function ignores dashes (`-`) in the given string. The remaining
/// letters must be exactly 32 hexadecimals, or an error will be raised.
///
pub fn from_hex(hex : String) -> UUID raise Error {
// XXX: ideally we should use strconv here, but we don't have sufficient
// tooling around String yet
let (hi, i) = for hi = 0L, i = 0, n = 0; n < 16; {
if i >= hex.length() {
fail("not enough hexadecimal in UUID string")
}
let c = hex.code_unit_at(i)
if c == '-' {
continue hi, i + 1, n
} else {
let d = hex_char_to_int64(c.to_int())
continue hi | (d << ((15 - n) * 4)), i + 1, n + 1
}
} nobreak {
(hi, i)
}
let (lo, i) = for lo = 0L, i = i, n = 0; n < 16; {
if i >= hex.length() {
fail("not enough hexadecimal in UUID string")
}
let c = hex.code_unit_at(i)
if c == '-' {
continue lo, i + 1, n
} else {
let d = hex_char_to_int64(c.to_int())
continue lo | (d << ((15 - n) * 4)), i + 1, n + 1
}
} nobreak {
(lo, i)
}
if i == hex.length() {
{ hi, lo }
} else {
fail("badly formed hexadecimal UUID string")
}
}
///|
#cfg(not(target="js"))
let hex_table : Array[Int] = "0123456789abcdef".to_array().map(c => c.to_int())
///|
#cfg(not(target="js"))
let dash : Int = '-'.to_int()
///|
#cfg(not(target="js"))
let segments : ReadOnlyArray[Int] = [8, 4, 4, 0, 4, 12]
// XXX: use sum() instead; weird `moon fmt`
///|
#cfg(not(target="js"))
let size : Int = (
segments.fold((acc, x) => acc + x, init=0) + segments.length() - 2
) *
2
///|
#cfg(not(target="js"))
pub fn UUID::to_string(self : UUID) -> String {
// This is a manually-composed UTF-16 string
let rv = FixedArray::make(size, b'\x00')
for v = self.lo, c = size - 1, i = size - 1, d = segments.length() - 1 {
let s = segments[d] * 2
if s == 0 {
continue self.hi, c, i, d - 1
} else if i > c - s {
rv[i - 1] = hex_table[(v & 0xfL).to_int()].to_byte()
continue (v.reinterpret_as_uint64() >> 4).reinterpret_as_int64(),
c,
i - 2,
d
} else if i > 0 {
rv[i - 1] = dash.to_byte()
continue v, i - 2, i - 2, d - 1
} else {
// `rv` is already the UTF-16 byte buffer we want. Reinterpret in
// place instead of going through Array::from_fixed_array +
// Bytes::from_array, which together account for ~58% of self
// time on a uuid round-trip benchmark.
break rv.unsafe_reinterpret_as_bytes().to_unchecked_string()
}
}
}
///|
#cfg(target="js")
pub fn UUID::to_string(self : UUID) -> String {
let hi = self.hi
.reinterpret_as_uint64()
.to_string(radix=16)
.pad_start(16, '0')
let lo = self.lo
.reinterpret_as_uint64()
.to_string(radix=16)
.pad_start(16, '0')
hi[:8].to_owned() +
"-" +
hi[8:12].to_owned() +
"-" +
hi[12:16].to_owned() +
"-" +
lo[:4].to_owned() +
"-" +
lo[4:].to_owned()
}
///|
pub impl Show for UUID with fn output(self : UUID, logger : &Logger) -> Unit {
logger.write_string(self.to_string())
}
///|
pub impl Debug for UUID with fn to_repr(self : UUID) -> Repr {
Repr::ctor("UUID", [(None, Repr::string(self.to_string()))])
}