chore: rebrand project to crank

This commit is contained in:
a.tolmachev
2026-03-28 00:58:56 +03:00
parent 6821d0c64a
commit 26335e8d9b
101 changed files with 550 additions and 538 deletions
+1
View File
@@ -0,0 +1 @@
pub mod to_schema;
+381
View File
@@ -0,0 +1,381 @@
use std::collections::BTreeMap;
use crank_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 crank_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(),
}
}
}
+11
View File
@@ -0,0 +1,11 @@
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 },
#[error("descriptor set could not be decoded")]
InvalidDescriptorSet,
#[error("descriptor pool could not be built")]
InvalidDescriptorPool,
}
+142
View File
@@ -0,0 +1,142 @@
pub mod convert;
pub mod errors;
pub mod model;
use prost::Message;
use prost_reflect::{
Cardinality, DescriptorPool, EnumDescriptor, FieldDescriptor, Kind, MessageDescriptor,
MethodDescriptor, ServiceDescriptor,
};
use prost_types::FileDescriptorSet;
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,
};
pub fn services_from_descriptor_set_bytes(bytes: &[u8]) -> Result<Vec<ProtoService>, ProtoError> {
let descriptor_set =
FileDescriptorSet::decode(bytes).map_err(|_| ProtoError::InvalidDescriptorSet)?;
let pool = DescriptorPool::from_file_descriptor_set(descriptor_set)
.map_err(|_| ProtoError::InvalidDescriptorPool)?;
Ok(pool.services().map(service_from_descriptor).collect())
}
fn service_from_descriptor(service: ServiceDescriptor) -> ProtoService {
ProtoService {
package: service.parent_file().package_name().to_owned(),
name: service.name().to_owned(),
methods: service.methods().map(method_from_descriptor).collect(),
}
}
fn method_from_descriptor(method: MethodDescriptor) -> ProtoMethod {
ProtoMethod {
name: method.name().to_owned(),
input: message_from_descriptor(method.input()),
output: message_from_descriptor(method.output()),
client_streaming: method.is_client_streaming(),
server_streaming: method.is_server_streaming(),
}
}
fn message_from_descriptor(message: MessageDescriptor) -> ProtoMessage {
let fields = message
.fields()
.filter(|field| field.containing_oneof().is_none())
.map(field_from_descriptor)
.collect();
let oneofs = message
.oneofs()
.map(|oneof| ProtoOneof {
name: oneof.name().to_owned(),
variants: oneof.fields().map(field_from_descriptor).collect(),
})
.collect();
ProtoMessage {
name: message.name().to_owned(),
fields,
oneofs,
}
}
fn field_from_descriptor(field: FieldDescriptor) -> ProtoField {
let cardinality = if field.is_list() && !field.is_map() {
ProtoFieldCardinality::Repeated
} else {
match field.cardinality() {
Cardinality::Optional => ProtoFieldCardinality::Optional,
Cardinality::Required => ProtoFieldCardinality::Required,
Cardinality::Repeated => ProtoFieldCardinality::Repeated,
}
};
ProtoField {
name: field.name().to_owned(),
json_name: field.json_name().to_owned(),
cardinality,
field_type: field_type_from_descriptor(field),
}
}
fn field_type_from_descriptor(field: FieldDescriptor) -> ProtoFieldType {
if field.is_map() {
return map_field_from_descriptor(field);
}
match field.kind() {
Kind::Bool => scalar_field(ProtoScalarKind::Bool),
Kind::String => scalar_field(ProtoScalarKind::String),
Kind::Bytes => scalar_field(ProtoScalarKind::Bytes),
Kind::Int32 | Kind::Sint32 | Kind::Sfixed32 => scalar_field(ProtoScalarKind::Int32),
Kind::Int64 | Kind::Sint64 | Kind::Sfixed64 => scalar_field(ProtoScalarKind::Int64),
Kind::Uint32 | Kind::Fixed32 => scalar_field(ProtoScalarKind::Uint32),
Kind::Uint64 | Kind::Fixed64 => scalar_field(ProtoScalarKind::Uint64),
Kind::Float => scalar_field(ProtoScalarKind::Float),
Kind::Double => scalar_field(ProtoScalarKind::Double),
Kind::Message(message) => ProtoFieldType::Message {
message: Box::new(message_from_descriptor(message)),
},
Kind::Enum(enumeration) => ProtoFieldType::Enum {
enumeration: enum_from_descriptor(enumeration),
},
}
}
fn map_field_from_descriptor(field: FieldDescriptor) -> ProtoFieldType {
let message = match field.kind() {
Kind::Message(message) => message,
_ => unreachable!(),
};
let key_field = message.map_entry_key_field();
let value_field = message.map_entry_value_field();
let key = match field_type_from_descriptor(key_field) {
ProtoFieldType::Scalar { scalar } => scalar,
_ => ProtoScalarKind::String,
};
ProtoFieldType::Map {
map: ProtoMapField {
key,
value: Box::new(field_type_from_descriptor(value_field)),
},
}
}
fn scalar_field(scalar: ProtoScalarKind) -> ProtoFieldType {
ProtoFieldType::Scalar { scalar }
}
fn enum_from_descriptor(enumeration: EnumDescriptor) -> ProtoEnum {
ProtoEnum {
name: enumeration.name().to_owned(),
values: enumeration
.values()
.map(|value| value.name().to_owned())
.collect(),
}
}
+54
View File
@@ -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,
}
+16
View File
@@ -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>,
}
+18
View File
@@ -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
}
}
+11
View File
@@ -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;
+16
View File
@@ -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())
}
}