diff --git a/foo.proto b/foo.proto index f856ff3..ff2852c 100644 --- a/foo.proto +++ b/foo.proto @@ -4,11 +4,6 @@ package core.todo.v1; message TodoListRequest {string user_id = 1;} -enum Error { - UNKNOWN = 0; - NOT_FOUND = 1; -} - message TodoAddRequest { string user_id = 1; string task = 2; diff --git a/src/config/config.rs b/src/config/config.rs index 0e73873..c04b571 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -74,7 +74,7 @@ pub struct Function { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, MergeRight)] pub struct ReturnTy { pub return_type: String, - pub error_type: String, + pub error_type: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, MergeRight)] diff --git a/src/config/to_wit.rs b/src/config/to_wit.rs index d010ffb..cfe14a4 100644 --- a/src/config/to_wit.rs +++ b/src/config/to_wit.rs @@ -170,7 +170,7 @@ impl Function { }; format!( - "func {}({}){}", + "{}: func({}){};", generate_wit_name(&self.name), params, return_type @@ -180,14 +180,10 @@ impl Function { impl ReturnTy { pub fn to_wit(&self) -> String { - if self.error_type.is_empty() { - self.return_type.clone() + if let Some(err) = self.error_type.as_ref() { + format!("result<{}, {}>", self.return_type, err) } else { - format!( - "result<{}, {}>", - self.return_type, - self.error_type - ) + format!("option<{}>", self.return_type) } } } diff --git a/src/config/wit_types.rs b/src/config/wit_types.rs index b755f3e..9db9e6d 100644 --- a/src/config/wit_types.rs +++ b/src/config/wit_types.rs @@ -40,6 +40,21 @@ pub enum WitType { } impl WitType { + pub fn from_primitive_proto_type(proto_ty: &str) -> Valid { + let binding = proto_ty.to_lowercase(); + let ty = binding.strip_prefix("type_").unwrap_or(proto_ty); + match ty { + "double" | "float" => Valid::succeed(WitType::Float64), + "int32" | "sint32" | "fixed32" | "sfixed32" => Valid::succeed(WitType::S32), + "int64" | "sint64" | "fixed64" | "sfixed64" => Valid::succeed(WitType::S64), + "uint32" => Valid::succeed(WitType::U32), + "uint64" => Valid::succeed(WitType::U64), + "bool" => Valid::succeed(WitType::Bool), + "string" => Valid::succeed(WitType::String), + // Ideally, this should never be reached + _ => Valid::fail(anyhow::anyhow!("Unknown/Complex type: {}", ty)), + } + } pub fn from_schema( schema: &Schema, openapi: &OpenApiSpec, diff --git a/src/proto/handle_services.rs b/src/proto/handle_services.rs new file mode 100644 index 0000000..60746de --- /dev/null +++ b/src/proto/handle_services.rs @@ -0,0 +1,88 @@ +use convert_case::Case; +use protox::prost_reflect::prost_types::{FileDescriptorSet, ServiceDescriptorProto}; +use tailcall_valid::{Valid, Validator}; +use crate::config::config::{Config, Function, Interface, Parameter, ReturnTy, UseStatement}; +use convert_case::Casing; +use crate::proto::proto::process_ty; + +fn handle_service(config: &Config, services: &[ServiceDescriptorProto]) -> Valid, anyhow::Error, anyhow::Error> { + Valid::from_iter(services.iter(), |service| { + let name = service.name().to_case(Case::Kebab); + Valid::from_iter(service.method.iter(), |method| { + let name = method.name().to_case(Case::Kebab); + let input_ty = if let Some(input) = method.input_type.as_ref() { + process_ty(input).map(|v| Some(v)) + } else { + Valid::succeed(None) + }; + + let output_ty = if let Some(output) = method.output_type.as_ref() { + process_ty(output).map(|v| Some(v)) + } else { + Valid::succeed(None) + }; + + input_ty.zip(output_ty).map(|(a, b)| { + let mut parameters = vec![]; + let mut return_type = ReturnTy { + return_type: "unit".to_string(), + + // TODO: I am not yet sure about error type + error_type: None, + }; + if let Some(a) = a { + // Protobuf only supports one input parameter, + // so we can assume that the name is "input" + parameters.push(Parameter { + name: "input".to_string(), + parameter_type: a.to_wit(None), + }); + } + if let Some(b) = b { + return_type.return_type = b.to_wit(None); + } + Function { + name, + parameters, + return_type, + } + }) + }).and_then(|functions| { + Valid::from_iter(config.interfaces.iter(), |interface| { + let use_name = interface.name.to_string(); + let mut imports = vec![]; + imports.extend(interface.records.iter().map(|v| v.name.to_string())); + imports.extend(interface.varients.iter().map(|(k, _)| k.to_string())); + + Valid::succeed( + UseStatement { + name: use_name, + items: imports, + } + ) + }).and_then(|uses| { + Valid::succeed(Interface { + name, + uses, + functions, + ..Default::default() + }) + }) + }) + }) +} + +pub fn handle_services(config: Config, proto: &[FileDescriptorSet]) -> Valid { + Valid::succeed(config) + .and_then(|mut config| { + Valid::from_iter(proto.iter(), |file| { + Valid::from_iter(file.file.iter(), |file| { + handle_service(&config, &file.service) + }) + .and_then(|interfaces| { + config.interfaces.extend(interfaces.into_iter().flatten().collect::>()); + Valid::succeed(()) + }) + }).and_then(|_| Valid::succeed(config)) + }) +} \ No newline at end of file diff --git a/src/proto/handle_types.rs b/src/proto/handle_types.rs index 1a3ddc1..c8df1f6 100644 --- a/src/proto/handle_types.rs +++ b/src/proto/handle_types.rs @@ -1,24 +1,49 @@ use std::collections::{BTreeMap, BTreeSet}; +use anyhow::anyhow; +use convert_case::Case; -use protox::prost_reflect::prost_types::{EnumDescriptorProto, FileDescriptorProto, FileDescriptorSet}; +use protox::prost_reflect::prost_types::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto, FileDescriptorSet}; use tailcall_valid::{Valid, Validator}; -use crate::config::config::{Config, Interface, Record}; +use crate::config::config::{Config, Field, Interface, Record}; use crate::config::wit_types::WitType; +use convert_case::Casing; +use crate::proto::proto::process_ty; -fn append_enums(_: &Config, file: &[EnumDescriptorProto]) -> Valid, anyhow::Error, anyhow::Error> { +fn append_enums(file: &[EnumDescriptorProto]) -> Valid, anyhow::Error, anyhow::Error> { Valid::from_iter(file.iter().enumerate(), |(i, enum_)| { - let enum_name = enum_.name(); + let enum_name = enum_.name().to_case(Case::Kebab); Valid::from_iter(enum_.value.iter().enumerate(), |(j, value)| { - Valid::succeed(value.name().to_string()) + Valid::succeed(value.name().to_case(Case::Kebab)) }).and_then(|varients| { - Valid::succeed((enum_name.to_string(), WitType::Enum(varients))) + Valid::succeed((enum_name, WitType::Enum(varients))) }) }).and_then(|rec| Valid::succeed(rec.into_iter().collect())) } -fn append_message(config: &Config, file: &FileDescriptorProto) -> Valid, anyhow::Error, anyhow::Error> { - Valid::succeed(vec![]) +fn append_message(messages: &[DescriptorProto]) -> Valid, anyhow::Error, anyhow::Error> { + Valid::from_iter(messages.iter(), |message| { + let record_name = message.name().to_case(Case::Kebab); + Valid::from_iter(message.field.iter().enumerate(), |(i, field)| { + if let Some(ty_) = field.type_name.as_ref() { + process_ty(ty_).map(|ty| (field.name().to_case(Case::Kebab), ty)) + } else { + Valid::from(WitType::from_primitive_proto_type(field.r#type().as_str_name())) + .map(|ty| (field.name().to_case(Case::Kebab), ty)) + }.and_then(|(name, ty)| { + Valid::succeed(Field { + name, + field_type: ty, + }) + }) + }).and_then(|fields| { + Valid::succeed(Record { + name: record_name, + fields: fields.into_iter().collect(), + ..Default::default() + }) + }) + }) } pub fn handle_types(config: Config, proto: &[FileDescriptorSet], package: String) -> Valid { @@ -28,8 +53,8 @@ pub fn handle_types(config: Config, proto: &[FileDescriptorSet], package: String let mut map = BTreeMap::new(); let mut records = BTreeSet::new(); Valid::from_iter(set.file.iter(), |file| { - append_enums(&config, &file.enum_type).and_then(|varients| { - append_message(&config, file) + append_enums(&file.enum_type).and_then(|varients| { + append_message(&file.message_type) .and_then(|recs| { map.extend(varients); records.extend(BTreeSet::from_iter(recs.into_iter())); diff --git a/src/proto/mod.rs b/src/proto/mod.rs index a1f1612..a8dc9eb 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -1,2 +1,3 @@ mod proto; -mod handle_types; \ No newline at end of file +mod handle_types; +mod handle_services; \ No newline at end of file diff --git a/src/proto/proto.rs b/src/proto/proto.rs index d513e6f..00e9b85 100644 --- a/src/proto/proto.rs +++ b/src/proto/proto.rs @@ -1,6 +1,9 @@ +use anyhow::anyhow; use protox::prost_reflect::prost_types::FileDescriptorSet; use tailcall_valid::{Valid, Validator}; use crate::config::config::Config; +use crate::config::wit_types::WitType; +use crate::proto::handle_services::handle_services; use crate::proto::handle_types::handle_types; pub struct Proto(Vec); @@ -12,6 +15,19 @@ impl Proto { pub fn to_config(&self) -> Valid { Valid::succeed(Config::default()) .and_then(|config| handle_types(config, &self.0, "api:todos@1.0.0".to_string())) + .and_then(|config| handle_services(config, &self.0)) + } +} + +pub fn process_ty(name: &str) -> Valid { + if !name.starts_with('.') { + return Valid::fail(anyhow!("Expected fully-qualified name for reference type but got {name}. This is a bug!")); + } + let name = &name[1..]; + if let Some((_package, name)) = name.rsplit_once('.') { + Valid::succeed(WitType::FieldTy(name.to_string())) + }else { + Valid::succeed(WitType::FieldTy(name.to_string())) } }