Skip to content

Commit 3fcd105

Browse files
committed
Add support for Option<T>
1 parent 367dd2d commit 3fcd105

File tree

5 files changed

+227
-0
lines changed

5 files changed

+227
-0
lines changed

rust/bufferfish-core/src/decodable.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,22 @@ impl<T: Decodable> Decodable for Vec<T> {
243243
T::max_bytes_allowed().map(|max_t_size| 2 + (u16::MAX as usize * max_t_size))
244244
}
245245
}
246+
247+
impl<T: Decodable> Decodable for Option<T> {
248+
fn decode_value(bf: &mut Bufferfish) -> Result<Option<T>, BufferfishError> {
249+
let flag = bf.read_u8()?;
250+
match flag {
251+
0 => Ok(None),
252+
1 => Ok(Some(T::decode_value(bf)?)),
253+
_ => Err(BufferfishError::InvalidEnumVariant),
254+
}
255+
}
256+
257+
fn min_bytes_required() -> Option<usize> {
258+
Some(1)
259+
}
260+
261+
fn max_bytes_allowed() -> Option<usize> {
262+
T::max_bytes_allowed().map(|max_t_size| 1 + max_t_size)
263+
}
264+
}

rust/bufferfish-core/src/encodable.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,15 @@ impl<T: Encodable> Encodable for Vec<T> {
105105
bf.write_array(self)
106106
}
107107
}
108+
109+
impl<T: Encodable> Encodable for Option<T> {
110+
fn encode_value(&self, bf: &mut Bufferfish) -> Result<(), BufferfishError> {
111+
match self {
112+
Some(value) => {
113+
bf.write_u8(1)?;
114+
value.encode_value(bf)
115+
}
116+
None => bf.write_u8(0),
117+
}
118+
}
119+
}

rust/bufferfish-core/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,27 @@ impl Bufferfish {
372372
Ok(())
373373
}
374374

375+
/// Writes an `Option<T>` to the buffer, where `T` implements the
376+
/// Encodable trait. If the option is `Some`, it will write a u8 with value
377+
/// 1, followed by the encoded value. If the option is `None`, it will write
378+
/// a u8 with value 0.
379+
pub fn write_option<T: Encodable>(
380+
&mut self,
381+
option: &Option<T>,
382+
) -> Result<(), BufferfishError> {
383+
match option {
384+
Some(value) => {
385+
self.write_u8(1)?;
386+
value.encode_value(self)?;
387+
}
388+
None => {
389+
self.write_u8(0)?;
390+
}
391+
}
392+
393+
Ok(())
394+
}
395+
375396
/// Writes an array of raw bytes to the buffer. Useful for encoding
376397
/// distinct structs into byte arrays and appending them to a buffer later.
377398
pub fn write_raw_bytes(&mut self, bytes: &[u8]) -> Result<(), BufferfishError> {
@@ -548,6 +569,19 @@ impl Bufferfish {
548569
}
549570
}
550571

572+
/// Reads an `Option<T>` from the buffer, where `T` implements the
573+
/// Decodable trait. If the option is `Some`, it will read a u8 with value
574+
/// 1, followed by the decoded value. If the option is `None`, it will read
575+
/// a u8 with value 0.
576+
pub fn read_option<T: Decodable>(bf: &mut Bufferfish) -> Result<Option<T>, BufferfishError> {
577+
let flag = bf.read_u8()?;
578+
match flag {
579+
0 => Ok(None),
580+
1 => Ok(Some(T::decode(bf)?)),
581+
_ => Err(BufferfishError::InvalidEnumVariant),
582+
}
583+
}
584+
551585
impl std::fmt::Display for Bufferfish {
552586
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
553587
let inner = self.inner.get_ref();

rust/bufferfish/src/compiler.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,24 @@ fn get_bufferfish_write_fn(ty: Type, value_accessor: &str) -> String {
381381
}
382382
}
383383

384+
if path.segments.len() == 1
385+
&& path.segments[0].ident == "Option"
386+
&& let PathArguments::AngleBracketed(ref args) = path.segments[0].arguments
387+
&& let Some(GenericArgument::Type(inner_ty)) = args.args.first()
388+
{
389+
let inner_write_call = if is_primitive_type(inner_ty) {
390+
let inner_write_fn = get_element_write_fn(inner_ty.clone());
391+
format!("bf.{inner_write_fn}({value_accessor})")
392+
} else {
393+
let inner_write_fn = get_element_write_fn(inner_ty.clone());
394+
format!("{inner_write_fn}(bf, {value_accessor})")
395+
};
396+
397+
return format!(
398+
"if ({value_accessor} !== null) {{\n bf.writeUint8(1)\n {inner_write_call}\n }} else {{\n bf.writeUint8(0)\n }}",
399+
);
400+
}
401+
384402
match path.get_ident().map(|ident| ident.to_string()).as_deref() {
385403
Some("u8") => format!("bf.writeUint8({value_accessor})"),
386404
Some("u16") => format!("bf.writeUint16({value_accessor})"),
@@ -491,6 +509,15 @@ fn get_typescript_type(ty: Type) -> String {
491509
return format!("Array<{inner_ts_type}>");
492510
}
493511

512+
if path.segments.len() == 1
513+
&& path.segments[0].ident == "Option"
514+
&& let PathArguments::AngleBracketed(ref args) = path.segments[0].arguments
515+
&& let Some(GenericArgument::Type(inner_ty)) = args.args.first()
516+
{
517+
let inner_ts_type = get_typescript_type(inner_ty.clone());
518+
return format!("{inner_ts_type} | null");
519+
}
520+
494521
match path.get_ident().map(|ident| ident.to_string()).as_deref() {
495522
#[rustfmt::skip]
496523
Some("u8") | Some("u16") | Some("u32") | Some("i8") | Some("i16") | Some("i32")
@@ -626,6 +653,18 @@ fn get_bufferfish_fn(ty: Type) -> String {
626653
return format!("bf.readArray(() => {inner_fn}) as Array<{inner_ts_type}>");
627654
}
628655

656+
if path.segments.len() == 1
657+
&& path.segments[0].ident == "Option"
658+
&& let PathArguments::AngleBracketed(ref args) = path.segments[0].arguments
659+
&& let Some(GenericArgument::Type(inner_ty)) = args.args.first()
660+
{
661+
let inner_fn = get_bufferfish_fn(inner_ty.clone());
662+
let inner_ts_type = get_typescript_type(inner_ty.clone());
663+
return format!(
664+
"(bf.readUint8() === 1 ? {inner_fn} : null) as {inner_ts_type} | null"
665+
);
666+
}
667+
629668
match path.get_ident().map(|ident| ident.to_string()).as_deref() {
630669
Some("u8") => "bf.readUint8() as number".to_string(),
631670
Some("u16") => "bf.readUint16() as number".to_string(),
@@ -699,6 +738,7 @@ mod tests {
699738
pub enum MessageId {
700739
Join = 0,
701740
Leave,
741+
Ping,
702742
Unknown = 255,
703743
}
704744
@@ -716,6 +756,12 @@ pub struct LeaveMessage;
716756
#[derive(Encode)]
717757
#[bufferfish(MessageId::Unknown)]
718758
pub struct UnknownMessage(pub u8, pub u16);
759+
760+
#[derive(Encode, Decode)]
761+
#[bufferfish(MessageId::Ping)]
762+
pub struct PingMessage {
763+
pub timestamp: Option<u64>,
764+
}
719765
"#;
720766

721767
let expected_output = r#"/* AUTOGENERATED BUFFERFISH FILE, DO NOT EDIT */
@@ -724,6 +770,7 @@ import { Bufferfish } from 'bufferfish'
724770
export enum MessageId {
725771
Join = 0,
726772
Leave = 1,
773+
Ping = 2,
727774
Unknown = 255,
728775
}
729776
@@ -770,6 +817,26 @@ export function encodeUnknownMessage(bf: Bufferfish, value: UnknownMessage): voi
770817
encodeMessageId(bf, MessageId.Unknown)
771818
bf.writeUint8(value[0])
772819
bf.writeUint16(value[1])
820+
}
821+
822+
export interface PingMessage {
823+
timestamp: bigint | null
824+
}
825+
826+
export function decodePingMessage(bf: Bufferfish): PingMessage {
827+
return {
828+
timestamp: (bf.readUint8() === 1 ? bf.readUint64() as bigint : null) as bigint | null,
829+
}
830+
}
831+
832+
export function encodePingMessage(bf: Bufferfish, value: PingMessage): void {
833+
encodeMessageId(bf, MessageId.Ping)
834+
if (value.timestamp !== null) {
835+
bf.writeUint8(1)
836+
bf.writeUint64(value.timestamp)
837+
} else {
838+
bf.writeUint8(0)
839+
}
773840
}"#;
774841

775842
let items = syn::parse_file(test_file)

rust/bufferfish/src/lib.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,4 +1121,99 @@ mod tests {
11211121
let decoded = CustomType::decode(&mut bf).unwrap();
11221122
assert_eq!(decoded.value, 42);
11231123
}
1124+
1125+
#[test]
1126+
fn test_optional_encode() {
1127+
use bufferfish_core as bufferfish;
1128+
use bufferfish_core::Encodable;
1129+
1130+
#[derive(Debug, Encode)]
1131+
struct OptionContainer {
1132+
value: Option<u32>,
1133+
}
1134+
1135+
let mut bf = Bufferfish::new();
1136+
let container_with_value = OptionContainer { value: Some(42) };
1137+
let container_without_value = OptionContainer { value: None };
1138+
1139+
container_with_value.encode(&mut bf).unwrap();
1140+
container_without_value.encode(&mut bf).unwrap();
1141+
1142+
assert_eq!(bf.as_ref(), &[1, 0, 0, 0, 42, 0]);
1143+
}
1144+
1145+
#[test]
1146+
fn test_optional_decode_some() {
1147+
use bufferfish_core as bufferfish;
1148+
use bufferfish_core::Decodable;
1149+
1150+
#[derive(Debug, Decode)]
1151+
struct OptionContainer {
1152+
value: Option<u32>,
1153+
}
1154+
1155+
let mut bf = Bufferfish::new();
1156+
bf.write_option(&Some(42)).unwrap();
1157+
1158+
let container_with_value = OptionContainer::decode(&mut bf).unwrap();
1159+
1160+
assert_eq!(container_with_value.value, Some(42));
1161+
}
1162+
1163+
#[test]
1164+
fn test_optional_decode_none() {
1165+
use bufferfish_core as bufferfish;
1166+
use bufferfish_core::Decodable;
1167+
1168+
#[derive(Debug, Decode)]
1169+
struct OptionContainer {
1170+
value: Option<u32>,
1171+
}
1172+
1173+
let mut bf = Bufferfish::new();
1174+
bf.write_option(&None::<u32>).unwrap();
1175+
1176+
let container_without_value = OptionContainer::decode(&mut bf).unwrap();
1177+
1178+
assert_eq!(container_without_value.value, None);
1179+
}
1180+
1181+
#[test]
1182+
fn test_encode_decode_with_option() {
1183+
use bufferfish_core as bufferfish;
1184+
use bufferfish_core::{Decodable, Encodable};
1185+
1186+
#[derive(Debug, Encode, Decode)]
1187+
struct User {
1188+
id: u32,
1189+
name: String,
1190+
age: Option<u8>,
1191+
}
1192+
1193+
let mut bf = Bufferfish::new();
1194+
let user_with_age = User {
1195+
id: 1,
1196+
name: "Bufferfish".to_string(),
1197+
age: Some(10),
1198+
};
1199+
let user_without_age = User {
1200+
id: 2,
1201+
name: "Bufferfish2".to_string(),
1202+
age: None,
1203+
};
1204+
1205+
user_with_age.encode(&mut bf).unwrap();
1206+
user_without_age.encode(&mut bf).unwrap();
1207+
1208+
let decoded_user_with_age = User::decode(&mut bf).unwrap();
1209+
let decoded_user_without_age = User::decode(&mut bf).unwrap();
1210+
1211+
assert_eq!(decoded_user_with_age.id, 1);
1212+
assert_eq!(decoded_user_with_age.name, "Bufferfish");
1213+
assert_eq!(decoded_user_with_age.age, Some(10));
1214+
1215+
assert_eq!(decoded_user_without_age.id, 2);
1216+
assert_eq!(decoded_user_without_age.name, "Bufferfish2");
1217+
assert_eq!(decoded_user_without_age.age, None);
1218+
}
11241219
}

0 commit comments

Comments
 (0)