382 lines
12 KiB
Rust
382 lines
12 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use mcpaas_schema::{Schema, SchemaKind};
|
|
|
|
use crate::{
|
|
errors::ProtoError,
|
|
model::{
|
|
ProtoEnum, ProtoField, ProtoFieldCardinality, ProtoFieldType, ProtoMapField, ProtoMessage,
|
|
ProtoOneof, ProtoScalarKind,
|
|
},
|
|
};
|
|
|
|
pub fn message_to_schema(message: &ProtoMessage) -> Result<Schema, ProtoError> {
|
|
let mut fields = BTreeMap::new();
|
|
|
|
for field in &message.fields {
|
|
let (name, schema) = convert_field(field)?;
|
|
fields.insert(name, schema);
|
|
}
|
|
|
|
for oneof in &message.oneofs {
|
|
let (name, schema) = convert_oneof(oneof)?;
|
|
fields.insert(name, schema);
|
|
}
|
|
|
|
Ok(object_schema(fields))
|
|
}
|
|
|
|
fn convert_field(field: &ProtoField) -> Result<(String, Schema), ProtoError> {
|
|
let mut schema = convert_field_type(&field.field_type)?;
|
|
|
|
schema.required = matches!(field.cardinality, ProtoFieldCardinality::Required);
|
|
schema.nullable = false;
|
|
|
|
if matches!(field.cardinality, ProtoFieldCardinality::Repeated) {
|
|
schema = Schema {
|
|
kind: SchemaKind::Array,
|
|
description: None,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: None,
|
|
fields: BTreeMap::new(),
|
|
items: Some(Box::new(schema)),
|
|
enum_values: Vec::new(),
|
|
variants: Vec::new(),
|
|
};
|
|
}
|
|
|
|
Ok((field.name.clone(), schema))
|
|
}
|
|
|
|
fn convert_field_type(field_type: &ProtoFieldType) -> Result<Schema, ProtoError> {
|
|
match field_type {
|
|
ProtoFieldType::Scalar { scalar } => Ok(scalar_schema(scalar)),
|
|
ProtoFieldType::Message { message } => message_to_schema(message),
|
|
ProtoFieldType::Enum { enumeration } => Ok(enum_schema(enumeration)),
|
|
ProtoFieldType::Map { map } => convert_map(map),
|
|
}
|
|
}
|
|
|
|
fn convert_map(map: &ProtoMapField) -> Result<Schema, ProtoError> {
|
|
let entry = object_schema(BTreeMap::from([
|
|
("key".to_owned(), scalar_schema(&map.key)),
|
|
("value".to_owned(), convert_field_type(&map.value)?),
|
|
]));
|
|
|
|
Ok(Schema {
|
|
kind: SchemaKind::Array,
|
|
description: None,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: None,
|
|
fields: BTreeMap::new(),
|
|
items: Some(Box::new(entry)),
|
|
enum_values: Vec::new(),
|
|
variants: Vec::new(),
|
|
})
|
|
}
|
|
|
|
fn convert_oneof(oneof: &ProtoOneof) -> Result<(String, Schema), ProtoError> {
|
|
if oneof.variants.is_empty() {
|
|
return Err(ProtoError::EmptyOneof {
|
|
oneof_name: oneof.name.clone(),
|
|
});
|
|
}
|
|
|
|
let mut variants = Vec::with_capacity(oneof.variants.len());
|
|
|
|
for field in &oneof.variants {
|
|
let (field_name, field_schema) = convert_field(field)?;
|
|
variants.push(object_schema(BTreeMap::from([(field_name, field_schema)])));
|
|
}
|
|
|
|
Ok((
|
|
oneof.name.clone(),
|
|
Schema {
|
|
kind: SchemaKind::Oneof,
|
|
description: None,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: None,
|
|
fields: BTreeMap::new(),
|
|
items: None,
|
|
enum_values: Vec::new(),
|
|
variants,
|
|
},
|
|
))
|
|
}
|
|
|
|
fn object_schema(fields: BTreeMap<String, Schema>) -> Schema {
|
|
Schema {
|
|
kind: SchemaKind::Object,
|
|
description: None,
|
|
required: true,
|
|
nullable: false,
|
|
default_value: None,
|
|
fields,
|
|
items: None,
|
|
enum_values: Vec::new(),
|
|
variants: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn enum_schema(enumeration: &ProtoEnum) -> Schema {
|
|
Schema {
|
|
kind: SchemaKind::Enum,
|
|
description: None,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: None,
|
|
fields: BTreeMap::new(),
|
|
items: None,
|
|
enum_values: enumeration.values.clone(),
|
|
variants: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn scalar_schema(kind: &ProtoScalarKind) -> Schema {
|
|
let schema_kind = match kind {
|
|
ProtoScalarKind::String | ProtoScalarKind::Bytes => SchemaKind::String,
|
|
ProtoScalarKind::Bool => SchemaKind::Boolean,
|
|
ProtoScalarKind::Int32
|
|
| ProtoScalarKind::Int64
|
|
| ProtoScalarKind::Uint32
|
|
| ProtoScalarKind::Uint64 => SchemaKind::Integer,
|
|
ProtoScalarKind::Float | ProtoScalarKind::Double => SchemaKind::Number,
|
|
};
|
|
|
|
Schema {
|
|
kind: schema_kind,
|
|
description: None,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: None,
|
|
fields: BTreeMap::new(),
|
|
items: None,
|
|
enum_values: Vec::new(),
|
|
variants: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use mcpaas_schema::SchemaKind;
|
|
|
|
use crate::{
|
|
ProtoEnum, ProtoField, ProtoFieldCardinality, ProtoFieldType, ProtoMapField, ProtoMessage,
|
|
ProtoMethod, ProtoOneof, ProtoScalarKind, ProtoService, message_to_schema,
|
|
};
|
|
|
|
#[test]
|
|
fn service_filters_unary_methods() {
|
|
let unary = ProtoMethod {
|
|
name: "GetLead".to_owned(),
|
|
input: empty_message("GetLeadRequest"),
|
|
output: empty_message("GetLeadResponse"),
|
|
client_streaming: false,
|
|
server_streaming: false,
|
|
};
|
|
|
|
let streaming = ProtoMethod {
|
|
name: "StreamLeads".to_owned(),
|
|
input: empty_message("StreamLeadsRequest"),
|
|
output: empty_message("StreamLeadsResponse"),
|
|
client_streaming: false,
|
|
server_streaming: true,
|
|
};
|
|
|
|
let service = ProtoService {
|
|
package: "crm.v1".to_owned(),
|
|
name: "LeadService".to_owned(),
|
|
methods: vec![unary.clone(), streaming],
|
|
};
|
|
|
|
let unary_methods = service.unary_methods().collect::<Vec<_>>();
|
|
|
|
assert_eq!(unary_methods.len(), 1);
|
|
assert_eq!(unary_methods[0].name, unary.name);
|
|
}
|
|
|
|
#[test]
|
|
fn converts_nested_message_to_schema() {
|
|
let request = ProtoMessage {
|
|
name: "CreateLeadRequest".to_owned(),
|
|
fields: vec![ProtoField {
|
|
name: "lead".to_owned(),
|
|
json_name: "lead".to_owned(),
|
|
cardinality: ProtoFieldCardinality::Required,
|
|
field_type: ProtoFieldType::Message {
|
|
message: Box::new(ProtoMessage {
|
|
name: "Lead".to_owned(),
|
|
fields: vec![ProtoField {
|
|
name: "email".to_owned(),
|
|
json_name: "email".to_owned(),
|
|
cardinality: ProtoFieldCardinality::Required,
|
|
field_type: ProtoFieldType::Scalar {
|
|
scalar: ProtoScalarKind::String,
|
|
},
|
|
}],
|
|
oneofs: Vec::new(),
|
|
}),
|
|
},
|
|
}],
|
|
oneofs: Vec::new(),
|
|
};
|
|
|
|
let schema = message_to_schema(&request).unwrap();
|
|
|
|
assert_eq!(schema.kind, SchemaKind::Object);
|
|
assert_eq!(
|
|
schema.field("lead").map(|field| field.kind.clone()),
|
|
Some(SchemaKind::Object)
|
|
);
|
|
assert_eq!(
|
|
schema
|
|
.field("lead")
|
|
.and_then(|field| field.field("email"))
|
|
.map(|field| field.kind.clone()),
|
|
Some(SchemaKind::String)
|
|
);
|
|
assert_eq!(
|
|
schema
|
|
.field("lead")
|
|
.and_then(|field| field.field("email"))
|
|
.map(|field| field.required),
|
|
Some(true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn converts_enum_map_and_oneof_contracts() {
|
|
let response = ProtoMessage {
|
|
name: "GetLeadResponse".to_owned(),
|
|
fields: vec![
|
|
ProtoField {
|
|
name: "status".to_owned(),
|
|
json_name: "status".to_owned(),
|
|
cardinality: ProtoFieldCardinality::Optional,
|
|
field_type: ProtoFieldType::Enum {
|
|
enumeration: ProtoEnum {
|
|
name: "LeadStatus".to_owned(),
|
|
values: vec!["OPEN".to_owned(), "CLOSED".to_owned()],
|
|
},
|
|
},
|
|
},
|
|
ProtoField {
|
|
name: "attributes".to_owned(),
|
|
json_name: "attributes".to_owned(),
|
|
cardinality: ProtoFieldCardinality::Optional,
|
|
field_type: ProtoFieldType::Map {
|
|
map: ProtoMapField {
|
|
key: ProtoScalarKind::String,
|
|
value: Box::new(ProtoFieldType::Scalar {
|
|
scalar: ProtoScalarKind::String,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
],
|
|
oneofs: vec![ProtoOneof {
|
|
name: "contact".to_owned(),
|
|
variants: vec![
|
|
ProtoField {
|
|
name: "email".to_owned(),
|
|
json_name: "email".to_owned(),
|
|
cardinality: ProtoFieldCardinality::Optional,
|
|
field_type: ProtoFieldType::Scalar {
|
|
scalar: ProtoScalarKind::String,
|
|
},
|
|
},
|
|
ProtoField {
|
|
name: "phone".to_owned(),
|
|
json_name: "phone".to_owned(),
|
|
cardinality: ProtoFieldCardinality::Optional,
|
|
field_type: ProtoFieldType::Scalar {
|
|
scalar: ProtoScalarKind::String,
|
|
},
|
|
},
|
|
],
|
|
}],
|
|
};
|
|
|
|
let schema = message_to_schema(&response).unwrap();
|
|
|
|
assert_eq!(
|
|
schema.field("status").map(|field| field.kind.clone()),
|
|
Some(SchemaKind::Enum)
|
|
);
|
|
assert_eq!(
|
|
schema
|
|
.field("status")
|
|
.map(|field| field.enum_values.clone())
|
|
.unwrap(),
|
|
vec!["OPEN".to_owned(), "CLOSED".to_owned()]
|
|
);
|
|
|
|
let attributes = schema.field("attributes").unwrap();
|
|
assert_eq!(attributes.kind, SchemaKind::Array);
|
|
assert_eq!(
|
|
attributes
|
|
.items
|
|
.as_deref()
|
|
.and_then(|entry| entry.field("key"))
|
|
.map(|field| field.kind.clone()),
|
|
Some(SchemaKind::String)
|
|
);
|
|
assert_eq!(
|
|
attributes
|
|
.items
|
|
.as_deref()
|
|
.and_then(|entry| entry.field("value"))
|
|
.map(|field| field.kind.clone()),
|
|
Some(SchemaKind::String)
|
|
);
|
|
|
|
let contact = schema.field("contact").unwrap();
|
|
assert_eq!(contact.kind, SchemaKind::Oneof);
|
|
assert_eq!(contact.variants.len(), 2);
|
|
assert_eq!(
|
|
contact.variants[0]
|
|
.field("email")
|
|
.map(|field| field.kind.clone()),
|
|
Some(SchemaKind::String)
|
|
);
|
|
assert_eq!(
|
|
contact.variants[1]
|
|
.field("phone")
|
|
.map(|field| field.kind.clone()),
|
|
Some(SchemaKind::String)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_empty_oneof() {
|
|
let message = ProtoMessage {
|
|
name: "BrokenMessage".to_owned(),
|
|
fields: Vec::new(),
|
|
oneofs: vec![ProtoOneof {
|
|
name: "selection".to_owned(),
|
|
variants: Vec::new(),
|
|
}],
|
|
};
|
|
|
|
let error = message_to_schema(&message).unwrap_err();
|
|
|
|
assert_eq!(
|
|
error,
|
|
crate::ProtoError::EmptyOneof {
|
|
oneof_name: "selection".to_owned(),
|
|
}
|
|
);
|
|
}
|
|
|
|
fn empty_message(name: &str) -> ProtoMessage {
|
|
ProtoMessage {
|
|
name: name.to_owned(),
|
|
fields: Vec::new(),
|
|
oneofs: Vec::new(),
|
|
}
|
|
}
|
|
}
|