oxiproto 0.1.2

Pure Rust protobuf toolkit (no protoc)
Documentation
// Integration test — gates on all relevant features being available.
// Run with: cargo test -p oxiproto --all-features
//
// This test validates:
//   1. OxiName provides correct full_name() and type_url()
//   2. OxiMessage encode/decode round-trip produces identical values
//   3. encoded_len() matches the actual byte length
//   4. OxiMessage wire bytes are bit-for-bit identical to prost-generated bytes
//   5. (build+codegen) codegen with emit_oxi_message_impl=true emits OxiMessage/OxiName impls

// Include prost-generated reference types for wire-byte cross-validation.
// OUT_DIR is set by Cargo during the build step.
include!(concat!(env!("OUT_DIR"), "/oxiproto.fixtures.rs"));

use oxiproto::{OxiMessage, OxiName, OxiProtoError};

// ─── Hand-written OxiMessage implementation for User ──────────────────────────
//
// This represents what oxiproto-codegen with emit_oxi_message_impl=true
// would generate for the User message in tests/fixtures/user.proto.

#[derive(Debug, Default, Clone, PartialEq)]
struct UserNative {
    id: i32,
    name: String,
    tags: Vec<String>,
    active: bool,
    _unknown: oxiproto::wire::UnknownFields,
}

impl OxiName for UserNative {
    const NAME: &'static str = "User";
    const PACKAGE: &'static str = "oxiproto.fixtures";
}

impl OxiMessage for UserNative {
    fn encoded_len(&self) -> usize {
        use oxiproto::wire::varint::encoded_len_varint;
        use oxiproto::wire::WireType;

        let mut len = 0usize;

        // field 1: id (int32, varint)
        if self.id != 0 {
            // tag: field 1, WireType::Varint = (1 << 3) | 0 = 8
            len += encoded_len_varint(8u64);
            len += encoded_len_varint(self.id as u64);
        }

        // field 2: name (string, len-delimited)
        if !self.name.is_empty() {
            // tag: field 2, WireType::Len = (2 << 3) | 2 = 18
            len += encoded_len_varint(18u64);
            let nb = self.name.len();
            len += encoded_len_varint(nb as u64);
            len += nb;
        }

        // field 3: tags (repeated string, len-delimited)
        for tag_str in &self.tags {
            // tag: field 3, WireType::Len = (3 << 3) | 2 = 26
            len += encoded_len_varint(26u64);
            let nb = tag_str.len();
            len += encoded_len_varint(nb as u64);
            len += nb;
        }

        // field 4: active (bool, varint)
        if self.active {
            // tag: field 4, WireType::Varint = (4 << 3) | 0 = 32
            len += encoded_len_varint(32u64);
            len += 1; // bool always encodes as 1 byte (0 or 1)
        }

        let _ = WireType::Varint; // silence unused-import lint
        len += self._unknown.encoded_len();
        len
    }

    fn encode_raw(&self, buf: &mut oxiproto::wire::EncodeBuffer) {
        use oxiproto::wire::WireType;

        // field 1: id (int32, varint)
        if self.id != 0 {
            buf.write_tag(1, WireType::Varint)
                .expect("write_tag field 1");
            // i32 is encoded as sign-extended i64 cast to u64 in proto3
            buf.write_varint(self.id as i64 as u64);
        }

        // field 2: name (string, len-delimited)
        if !self.name.is_empty() {
            buf.write_tag(2, WireType::Len).expect("write_tag field 2");
            buf.write_string(&self.name);
        }

        // field 3: tags (repeated string, len-delimited)
        for tag_str in &self.tags {
            buf.write_tag(3, WireType::Len).expect("write_tag field 3");
            buf.write_string(tag_str);
        }

        // field 4: active (bool, varint)
        if self.active {
            buf.write_tag(4, WireType::Varint)
                .expect("write_tag field 4");
            buf.write_bool(true);
        }

        // preserve unknown fields
        self._unknown.encode_to(buf);
    }

    fn merge(&mut self, buf: &mut oxiproto::wire::DecodeBuffer) -> oxiproto::OxiProtoResult<()> {
        use oxiproto::wire::WireType;

        loop {
            if buf.is_empty() {
                break;
            }

            let tag = match buf.read_tag() {
                Ok(t) => t,
                Err(oxiproto::wire::WireError::UnexpectedEof) => break,
                Err(e) => return Err(OxiProtoError::WireFormatError(e)),
            };

            match (tag.field_number, tag.wire_type) {
                (1, WireType::Varint) => {
                    self.id = buf.read_varint().map_err(OxiProtoError::WireFormatError)? as i32;
                }
                (2, WireType::Len) => {
                    let slice = buf
                        .read_length_delimited()
                        .map_err(OxiProtoError::WireFormatError)?;
                    self.name = String::from_utf8(slice.to_vec())
                        .map_err(|e| OxiProtoError::ParseError(e.to_string()))?;
                }
                (3, WireType::Len) => {
                    let slice = buf
                        .read_length_delimited()
                        .map_err(OxiProtoError::WireFormatError)?;
                    let s = String::from_utf8(slice.to_vec())
                        .map_err(|e| OxiProtoError::ParseError(e.to_string()))?;
                    self.tags.push(s);
                }
                (4, WireType::Varint) => {
                    self.active = buf.read_varint().map_err(OxiProtoError::WireFormatError)? != 0;
                }
                (_, wt) => {
                    // Skip unknown fields — collect them for preservation
                    match wt {
                        WireType::Varint => {
                            let v = buf.read_varint().map_err(OxiProtoError::WireFormatError)?;
                            self._unknown.push_varint(tag.field_number, v);
                        }
                        WireType::I64 => {
                            let v = buf.read_fixed64().map_err(OxiProtoError::WireFormatError)?;
                            self._unknown.push_fixed64(tag.field_number, v);
                        }
                        WireType::Len => {
                            let slice = buf
                                .read_length_delimited()
                                .map_err(OxiProtoError::WireFormatError)?;
                            self._unknown
                                .push_length_delimited(tag.field_number, slice.to_vec());
                        }
                        WireType::I32 => {
                            let v = buf.read_fixed32().map_err(OxiProtoError::WireFormatError)?;
                            self._unknown.push_fixed32(tag.field_number, v);
                        }
                        WireType::SGroup | WireType::EGroup => {
                            // Groups are deprecated; skip by advancing past them
                            buf.skip_field(wt).map_err(OxiProtoError::WireFormatError)?;
                        }
                    }
                }
            }
        }
        Ok(())
    }

    fn clear(&mut self) {
        self.id = 0;
        self.name.clear();
        self.tags.clear();
        self.active = false;
        self._unknown.clear();
    }
}

// ─── Tests ────────────────────────────────────────────────────────────────────

#[test]
fn oxi_name_correct() {
    assert_eq!(UserNative::full_name(), "oxiproto.fixtures.User");
    assert_eq!(
        UserNative::type_url(),
        "type.googleapis.com/oxiproto.fixtures.User"
    );
}

#[test]
fn oxi_message_encode_decode_round_trip() {
    let user = UserNative {
        id: 42,
        name: "Alice".to_owned(),
        tags: vec!["admin".to_owned(), "user".to_owned()],
        active: true,
        _unknown: Default::default(),
    };
    let bytes = oxiproto::encode(&user);
    let decoded = oxiproto::decode::<UserNative>(&bytes).expect("decode should succeed");
    assert_eq!(user, decoded);
}

#[test]
fn oxi_message_encode_empty_is_zero_bytes() {
    let user = UserNative::default();
    let bytes = oxiproto::encode(&user);
    assert!(
        bytes.is_empty(),
        "all-default proto3 message should encode to zero bytes"
    );
}

#[test]
fn encoded_len_matches_actual_length() {
    let user = UserNative {
        id: 1,
        name: "Bob".to_owned(),
        tags: vec!["x".to_owned()],
        active: false,
        _unknown: Default::default(),
    };
    let bytes = oxiproto::encode(&user);
    assert_eq!(
        user.encoded_len(),
        bytes.len(),
        "encoded_len() must equal the actual byte count"
    );
}

#[test]
fn wire_byte_cross_validation_vs_prost() {
    // Encode via OxiMessage (native hand-written impl)
    let oxi_user = UserNative {
        id: 99,
        name: "CrossVal".to_owned(),
        tags: vec!["a".to_owned(), "b".to_owned()],
        active: true,
        _unknown: Default::default(),
    };
    let oxi_bytes = oxiproto::encode(&oxi_user);

    // Encode via prost-generated User (from the include! above)
    use prost::Message as _;
    let prost_user = User {
        id: 99,
        name: "CrossVal".to_owned(),
        tags: vec!["a".to_owned(), "b".to_owned()],
        active: true,
    };
    let prost_bytes = prost_user.encode_to_vec();

    assert_eq!(
        oxi_bytes, prost_bytes,
        "OxiMessage and prost::Message must produce identical wire bytes"
    );
}

#[test]
fn decode_with_unknown_fields_preserves_round_trip() {
    // Encode via prost with an extra field that UserNative doesn't know about.
    // We do this by encoding manually: add field 99 (varint 123) to the end.
    use prost::Message as _;
    let prost_user = User {
        id: 5,
        name: "Unknown".to_owned(),
        tags: vec![],
        active: false,
    };
    let mut prost_bytes = prost_user.encode_to_vec();

    // Append a fake field 99 (varint wire type, value 42):
    // tag = (99 << 3) | 0 = 792 → varint: 0x98 0x06
    // value = 42 → varint: 0x2a
    prost_bytes.extend_from_slice(&[0x98, 0x06, 0x2a]);

    // Decode into UserNative — the unknown field should be preserved
    let decoded = oxiproto::decode::<UserNative>(&prost_bytes)
        .expect("decode with unknown fields should succeed");
    assert_eq!(decoded.id, 5);
    assert_eq!(decoded.name, "Unknown");
    assert!(
        !decoded._unknown.is_empty(),
        "unknown field 99 must be stored"
    );

    // Re-encode and verify that the unknown field is still in the bytes
    let re_encoded = oxiproto::encode(&decoded);
    assert_eq!(
        re_encoded.len(),
        prost_bytes.len(),
        "re-encoded bytes should have same length when unknown fields are preserved"
    );
}

#[cfg(all(feature = "build", feature = "codegen"))]
#[test]
fn codegen_with_oxi_message_impl_produces_valid_rust() {
    use std::time::SystemTime;

    let nanos = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .subsec_nanos();
    let temp_dir = std::env::temp_dir().join(format!("oxiproto-f-test-{nanos}"));
    std::fs::create_dir_all(&temp_dir).expect("create temp dir");

    let proto_path = temp_dir.join("test.proto");
    std::fs::write(
        &proto_path,
        b"syntax = \"proto3\";\nmessage Foo { int32 x = 1; }\n",
    )
    .expect("write proto");

    let fds = oxiproto::build::compile_to_fds(&[&proto_path], &[&temp_dir])
        .expect("compile_to_fds should succeed");

    let mut opts = oxiproto_codegen::CodegenOptions::new();
    opts.emit_oxi_message_impl = true;

    let code = oxiproto::codegen::generate_with_options(&fds, &opts)
        .expect("generate_with_options should succeed");

    assert!(
        code.contains("impl ::oxiproto_core::OxiMessage for Foo"),
        "Generated code should contain OxiMessage impl, got:\n{code}"
    );
    assert!(
        code.contains("impl ::oxiproto_core::OxiName for Foo"),
        "Generated code should contain OxiName impl, got:\n{code}"
    );

    let _ = std::fs::remove_dir_all(&temp_dir);
}