marshal function

List<int> marshal(
  1. Map<String, Object?> message,
  2. List<Map<String, Object?>> descriptor
)

Marshals a message (message) to protobuf binary bytes using descriptor.

Iterates over each field descriptor, looks up the corresponding value in message by name, and encodes it according to its type and label.

Returns the encoded protobuf bytes as a List<int>.

Implementation

List<int> marshal(
  Map<String, Object?> message,
  List<Map<String, Object?>> descriptor,
) {
  final buffer = <int>[];

  for (final field in descriptor) {
    final name = field['name'] as String;
    final fieldNumber = field['number'] as int;
    final type = field['type'] as String;
    final label = field['label'] as String?;
    final features = field['features'] as Map<String, String>?;
    final value = message[name];

    // Skip null values — field not present in the message map.
    if (value == null) continue;

    // Map fields: descriptor carries keyType/valueType metadata. Accept any
    // `Map` — binary unmarshal yields native-typed keys (`Map<Object?,Object?>`
    // keyed by e.g. `int`), while the JSON codec yields string keys; both must
    // round-trip through here.
    if (label == 'LABEL_REPEATED' &&
        type == 'TYPE_MESSAGE' &&
        value is Map &&
        field['mapEntry'] == true) {
      final keyType = field['keyType'] as String? ?? 'TYPE_STRING';
      final valueType = field['valueType'] as String? ?? 'TYPE_STRING';
      // For `map<K, message>` the value sub-message's field descriptors live on
      // the map field itself (set by the descriptor bridge); thread them through
      // so message-typed values re-marshal instead of failing.
      final valueDescriptor =
          field['messageDescriptor'] as List<Map<String, Object?>>?;
      marshalMapField(
        buffer,
        fieldNumber,
        value,
        keyType,
        valueType,
        valueDescriptor: valueDescriptor,
      );
      continue;
    }

    // Repeated fields.
    if (label == 'LABEL_REPEATED') {
      final list = value as List<Object?>;
      if (list.isEmpty) continue;

      if (type == 'TYPE_MESSAGE') {
        // Repeated messages are never packed — each is a separate record.
        // DELIMITED (group) encoding wraps each in START_GROUP/END_GROUP;
        // otherwise each is a length-prefixed (LEN) field.
        final delimited = features != null && isDelimited(features);
        final msgDescriptor =
            field['messageDescriptor'] as List<Map<String, Object?>>?;
        for (final item in list) {
          if (item == null) continue;
          final subBytes = msgDescriptor != null
              ? marshal(item as Map<String, Object?>, msgDescriptor)
              : item as List<int>;
          if (delimited) {
            encodeGroupField(buffer, fieldNumber, subBytes);
          } else {
            // Force-emit each element, including an empty submessage:
            // encodeMessageField would drop a zero-length one and misalign the
            // array (e.g. a repeated wrapper element holding the value 0).
            encodeTag(buffer, fieldNumber, _wtLen);
            encodeBytes(buffer, subBytes);
          }
        }
      } else if (type == 'TYPE_STRING') {
        // Repeated strings are not packed — each is a separate LEN field, and
        // every element is emitted (even ""), so indices stay aligned.
        for (final item in list) {
          if (item == null) continue;
          encodeTag(buffer, fieldNumber, _wtLen);
          encodeBytes(buffer, utf8.encode(item as String));
        }
      } else if (type == 'TYPE_BYTES') {
        // Repeated bytes are not packed — each is a separate LEN field, every
        // element emitted (even empty).
        for (final item in list) {
          if (item == null) continue;
          encodeTag(buffer, fieldNumber, _wtLen);
          encodeBytes(buffer, item as List<int>);
        }
      } else if (features != null && isExpandedRepeated(features)) {
        // Expanded scalar repeated (proto2 / EXPANDED override): each element
        // is its own tag-value record rather than one packed blob. Every
        // element is written, including default values (no proto3 elision).
        for (final item in list) {
          if (item == null) continue;
          _writeScalarForced(buffer, fieldNumber, type, item);
        }
      } else {
        // Packed scalar repeated field (proto3 / editions default).
        marshalRepeated(buffer, fieldNumber, type, list);
      }
      continue;
    }

    // Singular field. With EXPLICIT/LEGACY_REQUIRED presence the value is
    // serialized even when it equals the type default (proto3/IMPLICIT elides).
    // A *set* oneof member is also always serialized (its presence on the wire
    // is what selects the oneof case) — value==null was already skipped above,
    // so a present member here is a set one. A DELIMITED message field is
    // emitted as a group (wire type 3/4).
    final msgDescriptor =
        field['messageDescriptor'] as List<Map<String, Object?>>?;
    marshalField(
      buffer,
      fieldNumber,
      type,
      value,
      explicitPresence:
          field['oneof'] != null ||
          (features != null && hasExplicitPresence(features)),
      delimited: features != null && isDelimited(features),
      messageDescriptor: msgDescriptor,
    );
  }

  // Re-emit any retained unknown fields (raw tag+value bytes captured by
  // unmarshal), preserving them across a binary round-trip.
  final unknown = message[unknownFieldsKey];
  if (unknown is List<int>) buffer.addAll(unknown);

  return buffer;
}