// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// One field of a struct: the description it carries, and what the wire needs
/// that a description has no word for.
///
/// The description is a `@moonmodel.Field` — a name, a kind, whether the value
/// may be absent, and what it means — so that a type described once can be
/// written by this package and still be published as a JSON Schema or a
/// `CREATE TABLE` without being written down twice. The three other members are
/// the wire's own: the type ID that kind goes out as, the tag that identifies
/// the field when the schema configures one, and whether the value may be
/// shared.
pub(all) struct FieldMeta {
/// The description: name, kind, nullability, documentation.
desc : @moonmodel.Field
/// The type ID the description's kind goes out as.
ty : TypeId
/// The tag ID, when the schema configures one.
///
/// The specification allows `0 <= tag < 2^29` and requires tags to be unique
/// within a type; a tag sorts before every name, so adding one to a field
/// moves it.
tag : Int32?
/// Whether the value may be shared, and so preceded by the tracking byte
/// rather than the plain not-null one.
track_ref : Bool
/// The struct this field holds, when it holds one.
///
/// A field's type is declared, so the wire does not say it again: a nested
/// struct arrives as its fingerprint and its fields, with no type ID and no
/// user type ID in front. The schema has to know which struct the field is,
/// and this is where it says so — a field of type `Struct` with no child is
/// one this reader cannot follow.
child : StructMeta?
/// The element type of a list, when the schema declares one.
///
/// A writer that knows a list holds strings says so in the schema and leaves
/// the type out of the list header, which is a byte saved per list. Reading
/// such a list needs the type the schema left out, and this is where it is.
element : TypeId?
} derive(Eq, Debug)
///|
pub impl Show for FieldMeta with fn output(self, logger) {
logger.write_string("\{self.to_repr()}")
}
///|
pub extend FieldMeta with Debug::{to_repr}
///|
pub extend FieldMeta with Show::{to_string, output}
///|
pub extend FieldMeta with Eq::{not_equal, equal}
///|
/// A field that is simply there, named by hand.
///
/// `nullable` is a question about the value and goes into the description; a
/// kind the description has no word for is written as `Raw`, and the type ID
/// the caller gave stays the one it goes out as.
pub fn FieldMeta::new(
name : String,
ty : TypeId,
tag~ : Int32? = None,
nullable~ : Bool = false,
track_ref~ : Bool = false,
child~ : StructMeta? = None,
element~ : TypeId? = None,
) -> FieldMeta {
{
desc: @moonmodel.Field::new(name, ty.kind(), nullable~),
ty,
tag,
track_ref,
child,
element,
}
}
///|
/// The field a description asks for: its kind decides the type ID.
///
/// A kind with no wire type — `Any`, or a `Raw` that does not name one of this
/// format's own — is refused rather than guessed at, because a field whose type
/// the other side cannot name is a field neither side can read.
pub fn FieldMeta::from(
desc : @moonmodel.Field,
tag~ : Int32? = None,
track_ref~ : Bool = false,
child~ : StructMeta? = None,
element~ : TypeId? = None,
) -> FieldMeta raise {
{ desc, ty: TypeId::of_kind(desc.kind), tag, track_ref, child, element, }
}
///|
/// The field's name, which is what a peer reads it by.
pub fn FieldMeta::name(self : FieldMeta) -> String {
self.desc.name
}
///|
/// The kind the description gives this field.
pub fn FieldMeta::kind(self : FieldMeta) -> @moonmodel.Type {
self.desc.kind
}
///|
/// Whether the value may be absent. A nullable field is preceded by a flag byte
/// and sorts after every primitive that is not.
pub fn FieldMeta::nullable(self : FieldMeta) -> Bool {
self.desc.nullable
}
///|
/// The field's identifier as the order sees it: the tag when there is one, and
/// the `snake_case` of the name when there is not.
pub fn FieldMeta::identifier(self : FieldMeta) -> String {
match self.tag {
Some(t) => t.to_string()
None => snake(self.desc.name)
}
}
///|
/// `userName` as `user_name`, which is what the specification orders untagged
/// fields by.
///
/// A run of capitals stays one word, so `HTTPServer` is `http_server` and not
/// `h_t_t_p_server`.
fn snake(name : String) -> String {
let chars = name.to_array()
let out = StringBuilder()
let mut previous_was_word = false
for i in 0..= 'A' && c <= 'Z'
if upper {
let next_is_lower = i + 1 < chars.length() &&
chars[i + 1] >= 'a' &&
chars[i + 1] <= 'z'
if previous_was_word && (next_is_lower || !previous_was_word) {
out.write_char('_')
}
out.write_char(lower(c))
previous_was_word = true
} else {
let is_word = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
out.write_char(c)
previous_was_word = is_word
}
}
out.to_string()
}
///|
/// The same letter, lower: `Char` carries no case of its own in MoonBit.
fn lower(c : Char) -> Char {
if c >= 'A' && c <= 'Z' {
(c.to_int() + 32).unsafe_to_char()
} else {
c
}
}
///|
/// Which of two fields comes first, by the identifier alone: a tag before a
/// name, and two names in `snake_case` order.
fn compare_identifier(a : FieldMeta, b : FieldMeta) -> Int32 {
match (a.tag, b.tag) {
(Some(x), Some(y)) => if x < y { -1 } else if x > y { 1 } else { 0 }
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => compare_text(snake(a.desc.name), snake(b.desc.name))
}
}
///|
/// Two strings in the order a dictionary would put them, which is not the order
/// `String` compares in: that one answers length first.
fn compare_text(a : String, b : String) -> Int32 {
let x = a.to_array()
let y = b.to_array()
let n = if x.length() < y.length() { x.length() } else { y.length() }
for i in 0.. Int32 {
let group = fn(f : FieldMeta) -> Int32 {
if !f.ty.is_primitive() {
2
} else if f.nullable() {
1
} else {
0
}
}
let ga = group(a)
let gb = group(b)
if ga != gb {
return ga - gb
}
if ga < 2 {
let ca = if a.ty.is_compressed() { 1 } else { 0 }
let cb = if b.ty.is_compressed() { 1 } else { 0 }
if ca != cb {
return ca - cb
}
let sa = match a.ty.fixed_size() {
Some(n) => n
None => 0
}
let sb = match b.ty.fixed_size() {
Some(n) => n
None => 0
}
if sa != sb {
return sb - sa
}
if a.ty.code() != b.ty.code() {
return a.ty.code() - b.ty.code()
}
}
compare_identifier(a, b)
}
///|
/// A struct type: the description every projection shares, the user type ID it
/// is registered under, and one wire record per field.
///
/// The description is `@moonmodel.Model`, which is what `#model.derive` hands
/// back for a MoonBit type. The wire records are index-aligned with the model's
/// fields and carry the three things a description does not: the type ID, the
/// tag and the reference policy.
pub struct StructMeta {
/// The description: the type's name, its documentation, and its fields.
model_ : @moonmodel.Model
/// The user type ID written after the type ID byte in same-schema mode.
id_ : Int32
/// Per field, in declaration order: the type ID, the tag, whether it may be
/// shared, the struct it holds, and the element type of the list it holds.
wire_ : Array[(TypeId, Int32?, Bool, StructMeta?, TypeId?)]
} derive(Eq, Debug)
///|
pub impl Show for StructMeta with fn output(self, logger) {
logger.write_string("\{self.to_repr()}")
}
///|
pub extend StructMeta with Debug::{to_repr}
///|
pub extend StructMeta with Show::{to_string, output}
///|
pub extend StructMeta with Eq::{not_equal, equal}
///|
/// A struct described field by field, which is how a schema is written when
/// there is no `@moonmodel.Model` to hand.
///
/// The model is built from the fields: its name is `name`, its fields are the
/// descriptions the caller gave, in the order they were given.
pub fn StructMeta::new(
name : String,
user : Int32,
fields : Array[FieldMeta],
) -> StructMeta {
let descs : Array[@moonmodel.Field] = []
let wire : Array[(TypeId, Int32?, Bool, StructMeta?, TypeId?)] = []
for f in fields {
descs.push(f.desc)
wire.push((f.ty, f.tag, f.track_ref, f.child, f.element))
}
{ model_: @moonmodel.Model::new(name, descs), id_: user, wire_: wire, }
}
///|
/// A struct described by a model, which is what `#model.derive` produces.
///
/// `tags` names the fields whose identifier is a tag rather than a name, and
/// `refs` the fields whose value may be shared; both are empty in the ordinary
/// case, where a field is known by its name and sent as itself.
///
/// A field whose kind has no wire type is refused: a description this format
/// cannot carry is better named than silently dropped.
pub fn StructMeta::of(
model : @moonmodel.Model,
user~ : Int32,
tags~ : Map[String, Int32] = {},
refs~ : Array[String] = [],
children~ : Array[StructMeta] = [],
) -> StructMeta raise {
let wire : Array[(TypeId, Int32?, Bool, StructMeta?, TypeId?)] = []
for f in model.fields {
wire.push(
(
TypeId::of_kind(f.kind),
tags.get(f.name),
refs.contains(f.name),
child_of(f.kind, children),
element_of(f.kind),
),
)
}
{ model_: model, id_: user, wire_: wire, }
}
///|
/// The element type a kind declares, when it declares one: a list of a kind the
/// type mapping has a word for.
fn element_of(kind : @moonmodel.Type) -> TypeId? {
match kind {
List(inner) =>
match inner {
Any => None
_ => Some(TypeId::of_kind(inner)) catch { _ => None }
}
_ => None
}
}
///|
/// The struct a kind refers to, when one of the schemas the caller handed over
/// is it: by name for a `Ref`, and by being the one nested for a `Nest`.
fn child_of(
kind : @moonmodel.Type,
children : Array[StructMeta],
) -> StructMeta? {
match kind {
Ref(name) => {
let mut found = None
for c in children {
if c.name() == name {
found = Some(c)
}
}
found
}
Nest(model) => {
let mut found = None
for c in children {
if c.name() == model.name {
found = Some(c)
}
}
found
}
_ => None
}
}
///|
/// The struct's name, which a peer reading in compatible mode sees and which
/// this package writes down in its errors.
pub fn StructMeta::name(self : StructMeta) -> String {
self.model_.name
}
///|
/// The user type ID the struct is registered under.
pub fn StructMeta::user(self : StructMeta) -> Int32 {
self.id_
}
///|
/// The description, as the projections outside this package read it.
pub fn StructMeta::model(self : StructMeta) -> @moonmodel.Model {
self.model_
}
///|
/// The fields, in the order they are declared, each joined to its wire record.
pub fn StructMeta::fields(self : StructMeta) -> Array[FieldMeta] {
let out : Array[FieldMeta] = []
for i in 0.. Array[Int32] {
let fields = self.fields()
let indices : Array[Int32] = []
for i in 0.. Array[FieldMeta] {
let fields = self.fields()
let out : Array[FieldMeta] = []
for i in self.order() {
out.push(fields[i])
}
out
}
///|
/// The field a name refers to, which is what a reader needs when the order on
/// the wire is not the order it wants.
pub fn StructMeta::find(self : StructMeta, name : String) -> FieldMeta? {
for f in self.fields() {
if f.desc.name == name {
return Some(f)
}
}
None
}