Merge branch 'feat/proto-schema-bridge'
This commit is contained in:
@@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
### `feat/schema-engine`
|
### `feat/schema-engine`
|
||||||
|
|
||||||
Status: in_progress
|
Status: completed
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
|
|
||||||
- schema model exists
|
- schema model exists
|
||||||
- schema validation works
|
- schema validation works
|
||||||
- JSON sample normalization works
|
- JSON sample normalization works
|
||||||
|
- protobuf -> schema bridge contracts exist
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod to_schema;
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Error)]
|
||||||
|
pub enum ProtoError {
|
||||||
|
#[error("oneof {oneof_name} must contain at least one variant")]
|
||||||
|
EmptyOneof { oneof_name: String },
|
||||||
|
}
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
pub fn crate_name() -> &'static str {
|
pub mod convert;
|
||||||
"mcpaas-proto"
|
pub mod errors;
|
||||||
}
|
pub mod model;
|
||||||
|
|
||||||
|
pub use convert::to_schema::message_to_schema;
|
||||||
|
pub use errors::ProtoError;
|
||||||
|
pub use model::{
|
||||||
|
ProtoEnum, ProtoField, ProtoFieldCardinality, ProtoFieldType, ProtoMapField, ProtoMessage,
|
||||||
|
ProtoMethod, ProtoOneof, ProtoScalarKind, ProtoService,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::model::ProtoMessage;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ProtoScalarKind {
|
||||||
|
String,
|
||||||
|
Bool,
|
||||||
|
Int32,
|
||||||
|
Int64,
|
||||||
|
Uint32,
|
||||||
|
Uint64,
|
||||||
|
Float,
|
||||||
|
Double,
|
||||||
|
Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoEnum {
|
||||||
|
pub name: String,
|
||||||
|
pub values: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoMapField {
|
||||||
|
pub key: ProtoScalarKind,
|
||||||
|
pub value: Box<ProtoFieldType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum ProtoFieldType {
|
||||||
|
Scalar { scalar: ProtoScalarKind },
|
||||||
|
Message { message: Box<ProtoMessage> },
|
||||||
|
Enum { enumeration: ProtoEnum },
|
||||||
|
Map { map: ProtoMapField },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ProtoFieldCardinality {
|
||||||
|
Optional,
|
||||||
|
Required,
|
||||||
|
Repeated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoField {
|
||||||
|
pub name: String,
|
||||||
|
pub json_name: String,
|
||||||
|
pub cardinality: ProtoFieldCardinality,
|
||||||
|
pub field_type: ProtoFieldType,
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::model::ProtoField;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoOneof {
|
||||||
|
pub name: String,
|
||||||
|
pub variants: Vec<ProtoField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoMessage {
|
||||||
|
pub name: String,
|
||||||
|
pub fields: Vec<ProtoField>,
|
||||||
|
pub oneofs: Vec<ProtoOneof>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::model::ProtoMessage;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoMethod {
|
||||||
|
pub name: String,
|
||||||
|
pub input: ProtoMessage,
|
||||||
|
pub output: ProtoMessage,
|
||||||
|
pub client_streaming: bool,
|
||||||
|
pub server_streaming: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtoMethod {
|
||||||
|
pub fn is_unary(&self) -> bool {
|
||||||
|
!self.client_streaming && !self.server_streaming
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod field;
|
||||||
|
mod message;
|
||||||
|
mod method;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
pub use field::{
|
||||||
|
ProtoEnum, ProtoField, ProtoFieldCardinality, ProtoFieldType, ProtoMapField, ProtoScalarKind,
|
||||||
|
};
|
||||||
|
pub use message::{ProtoMessage, ProtoOneof};
|
||||||
|
pub use method::ProtoMethod;
|
||||||
|
pub use service::ProtoService;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::model::ProtoMethod;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtoService {
|
||||||
|
pub package: String,
|
||||||
|
pub name: String,
|
||||||
|
pub methods: Vec<ProtoMethod>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtoService {
|
||||||
|
pub fn unary_methods(&self) -> impl Iterator<Item = &ProtoMethod> {
|
||||||
|
self.methods.iter().filter(|method| method.is_unary())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -310,6 +310,15 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Нормализация protobuf-структур
|
||||||
|
|
||||||
|
Для protobuf -> schema bridge дополнительно фиксируются такие правила:
|
||||||
|
|
||||||
|
- `repeated` поле преобразуется в `array`;
|
||||||
|
- `enum` преобразуется в `type: enum` со списком `enum_values`;
|
||||||
|
- `map<K, V>` преобразуется в `array` объектов `{ key, value }`;
|
||||||
|
- `oneof` преобразуется в `type: oneof`, где каждый вариант представлен объектом с одним допустимым полем.
|
||||||
|
|
||||||
## 6. `MappingSet` и `MappingRule`
|
## 6. `MappingSet` и `MappingRule`
|
||||||
|
|
||||||
`MappingSet` - набор правил преобразования между внутренним MCP input/output и protocol-specific request/response model.
|
`MappingSet` - набор правил преобразования между внутренним MCP input/output и protocol-specific request/response model.
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ gRPC operation должна включать:
|
|||||||
- `.proto` и descriptor handling должны быть отделены от runtime-вызова;
|
- `.proto` и descriptor handling должны быть отделены от runtime-вызова;
|
||||||
- protobuf discovery не должен жить внутри gRPC adapter;
|
- protobuf discovery не должен жить внутри gRPC adapter;
|
||||||
- `oneof`, `enum`, `repeated`, `map` и well-known types требуют отдельной нормализации;
|
- `oneof`, `enum`, `repeated`, `map` и well-known types требуют отдельной нормализации;
|
||||||
|
- `map` на слое нормализованной schema модели представляется как `array` объектов вида `{ key, value }`;
|
||||||
|
- `oneof` на слое нормализованной schema модели представляется как `oneof` с вариантами-объектами, каждый из которых содержит одно допустимое поле;
|
||||||
- схема сообщения должна быть представлена в UI как обычная форма полей, а не как сырой protobuf descriptor;
|
- схема сообщения должна быть представлена в UI как обычная форма полей, а не как сырой protobuf descriptor;
|
||||||
- пользователь не должен видеть внутреннюю сложность protobuf-контракта больше, чем это нужно для настройки operation.
|
- пользователь не должен видеть внутреннюю сложность protobuf-контракта больше, чем это нужно для настройки operation.
|
||||||
- `JSONPath` используется как единый способ точечной адресации вложенных полей при настройке mapping поверх нормализованной JSON-модели.
|
- `JSONPath` используется как единый способ точечной адресации вложенных полей при настройке mapping поверх нормализованной JSON-модели.
|
||||||
|
|||||||
Reference in New Issue
Block a user