use crate::dynamic::schema_def::{FieldDefinition, FieldType, SchemaDefinition};
use crate::error::GermanicError;
use flatbuffers::FlatBufferBuilder;
use indexmap::IndexMap;
pub fn build_flatbuffer(
schema: &SchemaDefinition,
data: &serde_json::Value,
) -> Result<Vec<u8>, GermanicError> {
let obj = data
.as_object()
.ok_or_else(|| GermanicError::General("Root data must be a JSON object".into()))?;
let mut builder = FlatBufferBuilder::with_capacity(1024);
let root = build_table(&mut builder, &schema.fields, obj)?;
builder.finish_minimal(root);
Ok(builder.finished_data().to_vec())
}
enum PreparedField {
Absent,
Offset(u32),
Bool(bool, bool),
Int(i32, i32),
Float(f32, f32),
}
fn build_table(
builder: &mut FlatBufferBuilder<'_>,
fields: &IndexMap<String, FieldDefinition>,
data: &serde_json::Map<String, serde_json::Value>,
) -> Result<flatbuffers::WIPOffset<flatbuffers::TableFinishedWIPOffset>, GermanicError> {
let mut prepared: IndexMap<String, PreparedField> = IndexMap::new();
for (name, def) in fields {
let value = data.get(name);
let prep = prepare_field(builder, def, value)?;
prepared.insert(name.clone(), prep);
}
let table_start = builder.start_table();
for (index, (name, _def)) in fields.iter().enumerate() {
let voffset = 4 + (2 * index) as u16;
let prep = &prepared[name];
match prep {
PreparedField::Absent => {
}
PreparedField::Offset(raw) => {
builder.push_slot_always::<flatbuffers::WIPOffset<&str>>(
voffset,
flatbuffers::WIPOffset::new(*raw),
);
}
PreparedField::Bool(val, default) => {
builder.push_slot::<bool>(voffset, *val, *default);
}
PreparedField::Int(val, default) => {
builder.push_slot::<i32>(voffset, *val, *default);
}
PreparedField::Float(val, default) => {
builder.push_slot::<f32>(voffset, *val, *default);
}
}
}
Ok(builder.end_table(table_start))
}
fn prepare_field(
builder: &mut FlatBufferBuilder<'_>,
def: &FieldDefinition,
value: Option<&serde_json::Value>,
) -> Result<PreparedField, GermanicError> {
let Some(value) = value else {
return Ok(match &def.default {
Some(d) => match def.field_type {
FieldType::String => PreparedField::Offset(builder.create_string(d).value()),
FieldType::Bool => PreparedField::Bool(d.parse().unwrap_or(false), false),
FieldType::Int => PreparedField::Int(d.parse().unwrap_or(0), 0),
FieldType::Float => PreparedField::Float(d.parse().unwrap_or(0.0), 0.0),
_ => PreparedField::Absent,
},
None => PreparedField::Absent,
});
};
match def.field_type {
FieldType::String => {
let s = value.as_str().unwrap_or("");
Ok(PreparedField::Offset(builder.create_string(s).value()))
}
FieldType::Bool => {
let v = value.as_bool().unwrap_or(false);
let default: bool = def
.default
.as_ref()
.and_then(|d| d.parse().ok())
.unwrap_or(false);
Ok(PreparedField::Bool(v, default))
}
FieldType::Int => {
let v64 = value.as_i64().unwrap_or(0);
if v64 > i32::MAX as i64 || v64 < i32::MIN as i64 {
return Err(GermanicError::General(format!(
"Integer overflow: {} exceeds i32 range [{}, {}]",
v64,
i32::MIN,
i32::MAX
)));
}
let v = v64 as i32;
let default: i32 = def
.default
.as_ref()
.and_then(|d| d.parse().ok())
.unwrap_or(0);
Ok(PreparedField::Int(v, default))
}
FieldType::Float => {
let v64 = value.as_f64().unwrap_or(0.0);
let v = v64 as f32;
if v.is_infinite() && v64.is_finite() {
return Err(GermanicError::General(format!(
"Float overflow: {} exceeds f32 range",
v64
)));
}
let default: f32 = def
.default
.as_ref()
.and_then(|d| d.parse().ok())
.unwrap_or(0.0);
Ok(PreparedField::Float(v, default))
}
FieldType::StringArray => match value.as_array() {
Some(arr) if !arr.is_empty() => {
let offsets: Vec<_> = arr
.iter()
.map(|v| builder.create_string(v.as_str().unwrap_or("")))
.collect();
let vec_offset = builder.create_vector(&offsets);
Ok(PreparedField::Offset(vec_offset.value()))
}
_ => Ok(PreparedField::Absent),
},
FieldType::IntArray => match value.as_array() {
Some(arr) if !arr.is_empty() => {
let mut values = Vec::with_capacity(arr.len());
for v in arr {
let v64 = v.as_i64().unwrap_or(0);
if v64 > i32::MAX as i64 || v64 < i32::MIN as i64 {
return Err(GermanicError::General(format!(
"Integer overflow in array element: {} exceeds i32 range [{}, {}]",
v64,
i32::MIN,
i32::MAX
)));
}
values.push(v64 as i32);
}
let vec_offset = builder.create_vector(&values);
Ok(PreparedField::Offset(vec_offset.value()))
}
_ => Ok(PreparedField::Absent),
},
FieldType::Table => {
let nested_fields = def.fields.as_ref().ok_or_else(|| {
GermanicError::General("Table field has no nested field definitions".into())
})?;
match value.as_object() {
Some(obj) => {
let table_offset = build_table(builder, nested_fields, obj)?;
Ok(PreparedField::Offset(table_offset.value()))
}
None => Ok(PreparedField::Absent),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::schema_def::*;
use indexmap::IndexMap;
fn minimal_schema() -> SchemaDefinition {
let mut fields = IndexMap::new();
fields.insert(
"name".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
SchemaDefinition {
schema_id: "test.v1".into(),
version: 1,
fields,
}
}
#[test]
fn test_build_minimal() {
let schema = minimal_schema();
let data = serde_json::json!({ "name": "Hello" });
let bytes = build_flatbuffer(&schema, &data).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn test_build_with_bool() {
let mut fields = IndexMap::new();
fields.insert(
"name".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
fields.insert(
"active".into(),
FieldDefinition {
field_type: FieldType::Bool,
required: false,
default: Some("false".into()),
fields: None,
},
);
let schema = SchemaDefinition {
schema_id: "test.v1".into(),
version: 1,
fields,
};
let data = serde_json::json!({ "name": "Test", "active": true });
let bytes = build_flatbuffer(&schema, &data).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn test_build_with_nested() {
let mut addr_fields = IndexMap::new();
addr_fields.insert(
"street".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
addr_fields.insert(
"city".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
let mut fields = IndexMap::new();
fields.insert(
"name".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
fields.insert(
"address".into(),
FieldDefinition {
field_type: FieldType::Table,
required: true,
default: None,
fields: Some(addr_fields),
},
);
let schema = SchemaDefinition {
schema_id: "test.v1".into(),
version: 1,
fields,
};
let data = serde_json::json!({
"name": "Test",
"address": {
"street": "Main St",
"city": "Berlin"
}
});
let bytes = build_flatbuffer(&schema, &data).unwrap();
assert!(!bytes.is_empty());
assert!(bytes.len() > 20);
}
#[test]
fn test_build_int_overflow_rejected() {
let mut fields = IndexMap::new();
fields.insert(
"count".into(),
FieldDefinition {
field_type: FieldType::Int,
required: true,
default: None,
fields: None,
},
);
let schema = SchemaDefinition {
schema_id: "test.v1".into(),
version: 1,
fields,
};
let data = serde_json::json!({ "count": 3_000_000_000_i64 });
let result = build_flatbuffer(&schema, &data);
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("Integer overflow"),
"Must report integer overflow"
);
}
#[test]
fn test_build_int_negative_overflow_rejected() {
let mut fields = IndexMap::new();
fields.insert(
"count".into(),
FieldDefinition {
field_type: FieldType::Int,
required: true,
default: None,
fields: None,
},
);
let schema = SchemaDefinition {
schema_id: "test.v1".into(),
version: 1,
fields,
};
let data = serde_json::json!({ "count": -3_000_000_000_i64 });
let result = build_flatbuffer(&schema, &data);
assert!(result.is_err());
}
#[test]
fn test_build_with_string_array() {
let mut fields = IndexMap::new();
fields.insert(
"name".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
fields.insert(
"tags".into(),
FieldDefinition {
field_type: FieldType::StringArray,
required: false,
default: None,
fields: None,
},
);
let schema = SchemaDefinition {
schema_id: "test.v1".into(),
version: 1,
fields,
};
let data = serde_json::json!({ "name": "Test", "tags": ["a", "b", "c"] });
let bytes = build_flatbuffer(&schema, &data).unwrap();
assert!(!bytes.is_empty());
}
}