Skip to content

Commit

Permalink
Merge branch 'main' into feat/gen-tailcall-config-from-url
Browse files Browse the repository at this point in the history
  • Loading branch information
laststylebender14 authored May 21, 2024
2 parents 8272962 + 7c7633c commit 0147776
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/core/blueprint/from_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ where
"Int" => JsonSchema::Num,
"Boolean" => JsonSchema::Bool,
"Empty" => JsonSchema::Empty,
"JSON" => JsonSchema::Obj(HashMap::new()),
"JSON" => JsonSchema::Any,
_ => JsonSchema::Any,
}
};
Expand Down
69 changes: 48 additions & 21 deletions src/core/generator/from_proto.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashSet};

use anyhow::{bail, Result};
use derive_setters::Setters;
Expand All @@ -21,6 +21,9 @@ struct Context {

/// Root GraphQL query type
query: String,

/// Set of visited map types
map_types: HashSet<String>,
}

impl Context {
Expand All @@ -29,6 +32,7 @@ impl Context {
query: query.to_string(),
namespace: Default::default(),
config: Default::default(),
map_types: Default::default(),
}
}

Expand Down Expand Up @@ -64,10 +68,23 @@ impl Context {
fn append_msg_type(mut self, messages: &Vec<DescriptorProto>) -> Result<Self> {
for message in messages {
let msg_name = message.name();
if let Some(options) = message.options.as_ref() {
if options.map_entry.unwrap_or_default() {
continue;
}

let msg_type = GraphQLType::new(msg_name)
.extend(self.namespace.as_slice())
.into_object_type();

if message
.options
.as_ref()
.and_then(|opt| opt.map_entry)
.unwrap_or_default()
{
// map types in protobuf are encoded as nested type
// https://protobuf.dev/programming-guides/encoding/#maps
// since we encode it as JSON scalar type in graphQL
// record that this type is map and ignore it
self.map_types.insert(msg_type.id());
continue;
}

// first append the name of current message as namespace
Expand All @@ -77,10 +94,6 @@ impl Context {
// then drop it after handling nested types
self.namespace.pop();

let msg_type = GraphQLType::new(msg_name)
.extend(self.namespace.as_slice())
.into_object_type();

let mut ty = Type::default();
for field in message.field.iter() {
let field_name = GraphQLType::new(field.name())
Expand All @@ -93,19 +106,27 @@ impl Context {
cfg_field.list = label.contains("repeated");
cfg_field.required = label.contains("required") || cfg_field.list;

if field.type_name.is_some() {
// for non-primitive types
let type_of = graphql_type_from_ref(field.type_name())?
.into_object_type()
.to_string();
if let Some(type_name) = &field.type_name {
// check that current field is map.
// it's done by checking that we've seen this type before
// inside the nested type. It works only if we explore nested types
// before the current type
if self.map_types.contains(&type_name[1..]) {
cfg_field.type_of = "JSON".to_string();
// drop list option since it is not relevant
// when using JSON representation
cfg_field.list = false;
} else {
// for non-primitive types
let type_of = graphql_type_from_ref(type_name)?
.into_object_type()
.to_string();

cfg_field.type_of = type_of;
cfg_field.type_of = type_of;
}
} else {
let type_of = convert_primitive_type(field.r#type().as_str_name());
if type_of.eq("JSON") {
cfg_field.list = false;
cfg_field.required = false;
}

cfg_field.type_of = type_of;
}

Expand Down Expand Up @@ -207,8 +228,6 @@ fn convert_primitive_type(proto_ty: &str) -> String {
"int32" | "int64" | "fixed32" | "fixed64" | "uint32" | "uint64" => "Int",
"bool" => "Boolean",
"string" | "bytes" => "String",
"message" => "JSON", // JSON scalar is preloaded by tailcall, so there is no need to
// explicitly define it in the config.
x => x,
}
.to_string()
Expand Down Expand Up @@ -365,4 +384,12 @@ mod test {
insta::assert_snapshot!(config);
Ok(())
}

#[test]
fn test_map_types() -> Result<()> {
let set = compile_protobuf(&[protobuf::MAP])?;
let config = from_proto(&[set], "Query")?.to_sdl();
insta::assert_snapshot!(config);
Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: src/core/generator/from_proto.rs
expression: config
---
schema @server @upstream {
query: Query
}

input map__MapRequest @tag(id: "map.MapRequest") {
map: JSON!
}

type Query {
map__MapService__GetMap(mapRequest: map__MapRequest!): map__MapResponse! @grpc(body: "{{.args.mapRequest}}", method: "map.MapService.GetMap")
}

type map__MapResponse @tag(id: "map.MapResponse") {
map: JSON!
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type person__Person @tag(id: "person.Person") {
id: Int!
name: String!
phone: [person__PhoneNumber]!
stringMap: [person__Person__StringMapEntry]!
stringMap: JSON!
}

type person__PhoneNumber @tag(id: "person.PhoneNumber") {
Expand Down
28 changes: 28 additions & 0 deletions src/core/grpc/protobuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,32 @@ pub mod tests {

Ok(())
}

#[tokio::test]
async fn map_proto_file() -> Result<()> {
let grpc_method = GrpcMethod::try_from("map.MapService.GetMap").unwrap();

let file = ProtobufSet::from_proto_file(get_proto_file(protobuf::MAP).await?)?;
let service = file.find_service(&grpc_method)?;
let operation = service.find_operation(&grpc_method)?;

// only single key-value in json since the converted output can change the
// ordering on every run
let input = operation.convert_input(r#"{ "map": { "key": "value" } }"#)?;

assert_eq!(input, b"\0\0\0\0\x0e\n\x0c\n\x03key\x12\x05value");

let output = b"\0\0\0\0\x12\n\t\x08\x01\x12\x05value\n\x05\x08\x02\x12\x01v";

let parsed = operation.convert_output::<serde_json::Value>(output)?;

assert_eq!(
serde_json::to_value(parsed)?,
json!({
"map": { "1": "value", "2": "v" }
})
);

Ok(())
}
}
5 changes: 5 additions & 0 deletions src/core/json/json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ impl TryFrom<&MessageDescriptor> for JsonSchema {
type Error = crate::core::valid::ValidationError<String>;

fn try_from(value: &MessageDescriptor) -> Result<Self, Self::Error> {
if value.is_map_entry() {
// we encode protobuf's map as JSON scalar
return Ok(JsonSchema::Any);
}

let mut map = std::collections::HashMap::new();
let fields = value.fields();

Expand Down
15 changes: 15 additions & 0 deletions tailcall-fixtures/fixtures/protobuf/map.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
syntax = "proto3";

package map;

message MapRequest {
map<string, string> map = 1;
}

message MapResponse {
map<int32, string> map = 1;
}

service MapService {
rpc GetMap (MapRequest) returns (MapResponse) {}
}
19 changes: 19 additions & 0 deletions tests/core/snapshots/grpc-map.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"map__MapService__GetMap": {
"map": {
"1": "value"
}
}
}
}
}
31 changes: 31 additions & 0 deletions tests/core/snapshots/grpc-map.md_client.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
source: tests/core/spec.rs
expression: client
---
scalar Date

scalar Email

scalar Empty

scalar JSON

scalar PhoneNumber

type Query {
map__MapService__GetMap(mapRequest: map__MapRequest!): map__MapResponse!
}

scalar Url

input map__MapRequest {
map: JSON!
}

type map__MapResponse {
map: JSON!
}

schema {
query: Query
}
19 changes: 19 additions & 0 deletions tests/core/snapshots/grpc-map.md_merged.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: tests/core/spec.rs
expression: merged
---
schema @server(port: 8000) @upstream(baseURL: "http://localhost:50051", batch: {delay: 10, headers: []}, httpCache: true) @link(src: "map.proto", type: Protobuf) {
query: Query
}

input map__MapRequest {
map: JSON!
}

type Query {
map__MapService__GetMap(mapRequest: map__MapRequest!): map__MapResponse! @grpc(body: "{{.args.mapRequest}}", method: "map.MapService.GetMap")
}

type map__MapResponse @tag(id: "map.MapResponse") {
map: JSON!
}
67 changes: 67 additions & 0 deletions tests/execution/grpc-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Grpc map type

```protobuf @file:map.proto
syntax = "proto3";
package map;
message MapRequest {
map<string, string> map = 1;
}
message MapResponse {
map<int32, string> map = 1;
}
service MapService {
rpc GetMap (MapRequest) returns (MapResponse) {}
}
```

```graphql @config
schema
@server(port: 8000)
@upstream(baseURL: "http://localhost:50051", httpCache: true, batch: {delay: 10})
@link(src: "map.proto", type: Protobuf) {
query: Query
}

schema @server @upstream {
query: Query
}

input map__MapRequest @tag(id: "map.MapRequest") {
map: JSON!
}

type Query {
map__MapService__GetMap(mapRequest: map__MapRequest!): map__MapResponse!
@grpc(body: "{{.args.mapRequest}}", method: "map.MapService.GetMap")
}

type map__MapResponse @tag(id: "map.MapResponse") {
map: JSON!
}
```

```yml @mock
- request:
method: POST
url: http://localhost:50051/map.MapService/GetMap
response:
status: 200
textBody: \0\0\0\0\x12\n\t\x08\x01\x12\x05value
```

```yml @test
- method: POST
url: http://localhost:8080/graphql
body:
query: >
query {
map__MapService__GetMap(mapRequest: { map: { key: "value" } }) {
map
}
}
```

0 comments on commit 0147776

Please sign in to comment.