diff --git a/src/core/generator/from_proto.rs b/src/core/generator/from_proto.rs index 940df1fc4d..844529ad6f 100644 --- a/src/core/generator/from_proto.rs +++ b/src/core/generator/from_proto.rs @@ -3,10 +3,13 @@ use std::collections::{BTreeSet, HashSet}; use anyhow::{bail, Result}; use derive_setters::Setters; use prost_reflect::prost_types::{ - DescriptorProto, EnumDescriptorProto, FileDescriptorSet, ServiceDescriptorProto, + DescriptorProto, EnumDescriptorProto, FileDescriptorSet, ServiceDescriptorProto, SourceCodeInfo, }; use super::graphql_type::{GraphQLType, Unparsed}; +use super::proto::comments_builder::CommentsBuilder; +use super::proto::path_builder::PathBuilder; +use super::proto::path_field::PathField; use crate::core::config::{Arg, Config, Enum, Field, Grpc, Tag, Type}; /// Assists in the mapping and retrieval of proto type names to custom formatted @@ -24,6 +27,10 @@ struct Context { /// Set of visited map types map_types: HashSet, + + /// Optional field to store source code information, including comments, for + /// each entity. + comments_builder: CommentsBuilder, } impl Context { @@ -33,9 +40,16 @@ impl Context { namespace: Default::default(), config: Default::default(), map_types: Default::default(), + comments_builder: CommentsBuilder::new(None), } } + /// Sets source code information for preservation of comments. + fn with_source_code_info(mut self, source_code_info: SourceCodeInfo) -> Self { + self.comments_builder = CommentsBuilder::new(Some(source_code_info)); + self + } + /// Resolves the actual name and inserts the type. fn insert_type(mut self, name: String, ty: Type) -> Self { self.config.types.insert(name.to_string(), ty); @@ -43,30 +57,67 @@ impl Context { } /// Processes proto enum types. - fn append_enums(mut self, enums: &Vec) -> Self { - for enum_ in enums { + fn append_enums( + mut self, + enums: &[EnumDescriptorProto], + parent_path: &PathBuilder, + is_nested: bool, + ) -> Self { + for (index, enum_) in enums.iter().enumerate() { let enum_name = enum_.name(); - let variants = enum_ - .value - .iter() - .map(|v| GraphQLType::new(v.name()).into_enum_variant().to_string()) - .collect::>(); + let enum_type_path = if is_nested { + parent_path.extend(PathField::NestedEnum, index as i32) + } else { + parent_path.extend(PathField::EnumType, index as i32) + }; + + let mut variants_with_comments = BTreeSet::new(); + + for (value_index, v) in enum_.value.iter().enumerate() { + let variant_name = GraphQLType::new(v.name()).into_enum_variant().to_string(); + + // Path to the enum value's comments + let value_path = PathBuilder::new(&enum_type_path) + .extend(PathField::EnumValue, value_index as i32); // 2: value field + + // Get comments for the enum value + let comment = self.comments_builder.get_comments(&value_path); + + // Format the variant with its comment as description + if let Some(comment) = comment { + // TODO: better support for enum variant descriptions [There is no way to define + // description for enum variant in current config structure] + let variant_with_comment = + format!("\"\"\n {}\n \"\"\n {}", comment, variant_name); + variants_with_comments.insert(variant_with_comment); + } else { + variants_with_comments.insert(variant_name); + } + } let type_name = GraphQLType::new(enum_name) .extend(self.namespace.as_slice()) .into_enum() .to_string(); + + let doc = self.comments_builder.get_comments(&enum_type_path); + self.config .enums - .insert(type_name, Enum { variants, doc: None }); + .insert(type_name, Enum { variants: variants_with_comments, doc }); } self } /// Processes proto message types. - fn append_msg_type(mut self, messages: &Vec) -> Result { - for message in messages { + fn append_msg_type( + mut self, + messages: &[DescriptorProto], + parent_path: &PathBuilder, + is_nested: bool, + ) -> Result { + for (index, message) in messages.iter().enumerate() { let msg_name = message.name(); let msg_type = GraphQLType::new(msg_name) @@ -87,15 +138,26 @@ impl Context { continue; } + let msg_path = if is_nested { + parent_path.extend(PathField::NestedType, index as i32) + } else { + parent_path.extend(PathField::MessageType, index as i32) + }; + // first append the name of current message as namespace self.namespace.push(msg_name.to_string()); - self = self.append_enums(&message.enum_type); - self = self.append_msg_type(&message.nested_type)?; + self = self.append_enums(&message.enum_type, &PathBuilder::new(&msg_path), true); + self = + self.append_msg_type(&message.nested_type, &PathBuilder::new(&msg_path), true)?; // then drop it after handling nested types self.namespace.pop(); - let mut ty = Type::default(); - for field in message.field.iter() { + let mut ty = Type { + doc: self.comments_builder.get_comments(&msg_path), + ..Default::default() + }; + + for (field_index, field) in message.field.iter().enumerate() { let field_name = GraphQLType::new(field.name()) .extend(self.namespace.as_slice()) .into_field(); @@ -131,6 +193,10 @@ impl Context { cfg_field.type_of = type_of; } + let field_path = + PathBuilder::new(&msg_path).extend(PathField::Field, field_index as i32); + cfg_field.doc = self.comments_builder.get_comments(&field_path); + ty.fields.insert(field_name.to_string(), cfg_field); } @@ -142,14 +208,20 @@ impl Context { } /// Processes proto service definitions and their methods. - fn append_query_service(mut self, services: &Vec) -> Result { + fn append_query_service( + mut self, + services: &[ServiceDescriptorProto], + parent_path: &PathBuilder, + ) -> Result { if services.is_empty() { return Ok(self); } - for service in services { + for (index, service) in services.iter().enumerate() { let service_name = service.name(); - for method in &service.method { + let path = parent_path.extend(PathField::Service, index as i32); + + for (method_index, method) in service.method.iter().enumerate() { let field_name = GraphQLType::new(method.name()) .extend(self.namespace.as_slice()) .push(service_name) @@ -190,6 +262,10 @@ impl Context { method: field_name.id(), }); + let method_path = + PathBuilder::new(&path).extend(PathField::Method, method_index as i32); + cfg_field.doc = self.comments_builder.get_comments(&method_path); + let ty = self .config .types @@ -263,10 +339,16 @@ pub fn from_proto(descriptor_sets: &[FileDescriptorSet], query: &str) -> Result< for file_descriptor in descriptor_set.file.iter() { ctx.namespace = vec![file_descriptor.package().to_string()]; + if let Some(source_code_info) = &file_descriptor.source_code_info { + ctx = ctx.with_source_code_info(source_code_info.clone()); + } + + let root_path = PathBuilder::new(&[]); + ctx = ctx - .append_enums(&file_descriptor.enum_type) - .append_msg_type(&file_descriptor.message_type)? - .append_query_service(&file_descriptor.service)?; + .append_enums(&file_descriptor.enum_type, &root_path, false) + .append_msg_type(&file_descriptor.message_type, &root_path, false)? + .append_query_service(&file_descriptor.service, &root_path)?; } } diff --git a/src/core/generator/mod.rs b/src/core/generator/mod.rs index 95dfc4d105..a33e55ace7 100644 --- a/src/core/generator/mod.rs +++ b/src/core/generator/mod.rs @@ -1,6 +1,7 @@ mod from_proto; mod generator; mod graphql_type; +mod proto; mod source; pub use generator::Generator; pub use source::Source; diff --git a/src/core/generator/proto/comments_builder.rs b/src/core/generator/proto/comments_builder.rs new file mode 100644 index 0000000000..34156ac232 --- /dev/null +++ b/src/core/generator/proto/comments_builder.rs @@ -0,0 +1,28 @@ +use prost_reflect::prost_types::SourceCodeInfo; + +pub struct CommentsBuilder { + source_code_info: Option, +} + +impl CommentsBuilder { + pub fn new(source_code_info: Option) -> Self { + Self { source_code_info } + } + + pub fn get_comments(&self, path: &[i32]) -> Option { + self.source_code_info.as_ref().and_then(|info| { + info.location + .iter() + .find(|loc| loc.path == path) + .and_then(|loc| { + loc.leading_comments.as_ref().map(|c| { + c.lines() + .map(|line| line.trim_start_matches('*').trim()) + .filter(|line| !line.is_empty()) + .collect::>() + .join("\n ") + }) + }) + }) + } +} diff --git a/src/core/generator/proto/mod.rs b/src/core/generator/proto/mod.rs new file mode 100644 index 0000000000..c0a20dc3d4 --- /dev/null +++ b/src/core/generator/proto/mod.rs @@ -0,0 +1,3 @@ +pub mod comments_builder; +pub mod path_builder; +pub mod path_field; diff --git a/src/core/generator/proto/path_builder.rs b/src/core/generator/proto/path_builder.rs new file mode 100644 index 0000000000..a65b050833 --- /dev/null +++ b/src/core/generator/proto/path_builder.rs @@ -0,0 +1,18 @@ +use super::path_field::PathField; + +pub struct PathBuilder { + base_path: Vec, +} + +impl PathBuilder { + pub fn new(base_path: &[i32]) -> Self { + Self { base_path: base_path.to_vec() } + } + + pub fn extend(&self, field: PathField, index: i32) -> Vec { + let mut extended_path = self.base_path.clone(); + extended_path.push(field.value()); + extended_path.push(index); + extended_path + } +} diff --git a/src/core/generator/proto/path_field.rs b/src/core/generator/proto/path_field.rs new file mode 100644 index 0000000000..c2c8ecaf70 --- /dev/null +++ b/src/core/generator/proto/path_field.rs @@ -0,0 +1,25 @@ +pub enum PathField { + EnumType, + MessageType, + Service, + Field, + Method, + EnumValue, + NestedType, + NestedEnum, +} + +impl PathField { + pub fn value(&self) -> i32 { + match self { + PathField::EnumType => 5, + PathField::MessageType => 4, + PathField::Service => 6, + PathField::Field => 2, + PathField::Method => 2, + PathField::EnumValue => 2, + PathField::NestedType => 3, + PathField::NestedEnum => 4, + } + } +} diff --git a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__from_proto.snap b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__from_proto.snap index 394812e4bc..e2701a2da0 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__from_proto.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__from_proto.snap @@ -10,6 +10,9 @@ input greetings__HelloRequest @tag(id: "greetings.HelloRequest") { name: String } + """ + The request message containing the user's name. + """ input greetings_a__b__HelloRequest @tag(id: "greetings_a.b.HelloRequest") { name: String } @@ -37,7 +40,13 @@ enum news__Status { } type Query { + """ + Sends a greeting + """ greetings_a__b__Greeter__SayHello(helloRequest: greetings_a__b__HelloRequest!): greetings_a__b__HelloReply! @grpc(body: "{{.args.helloRequest}}", method: "greetings_a.b.Greeter.SayHello") + """ + Sends a greeting + """ greetings_b__c__Greeter__SayHello(helloRequest: greetings__HelloRequest!): greetings__HelloReply! @grpc(body: "{{.args.helloRequest}}", method: "greetings_b.c.Greeter.SayHello") news__NewsService__AddNews(news: news__NewsInput!): news__News! @grpc(body: "{{.args.news}}", method: "news.NewsService.AddNews") news__NewsService__DeleteNews(newsId: news__NewsId!): Empty! @grpc(body: "{{.args.newsId}}", method: "news.NewsService.DeleteNews") @@ -51,6 +60,9 @@ type greetings__HelloReply @tag(id: "greetings.HelloReply") { message: String } + """ + The response message containing the greetings + """ type greetings_a__b__HelloReply @tag(id: "greetings_a.b.HelloReply") { message: String } diff --git a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__movies.snap b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__movies.snap index 81e3798d49..8bc6d05eb1 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__movies.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__movies.snap @@ -6,30 +6,193 @@ schema @server @upstream { query: Query } + """ + A Duration represents a signed, fixed-length span of time represented + as a count of seconds and fractions of seconds at nanosecond + resolution. It is independent of any calendar and concepts like "day" + or "month". It is related to Timestamp in that the difference between + two Timestamp values is a Duration and it can be added or subtracted + from a Timestamp. Range is approximately +-10,000 years. + # Examples + Example 1: Compute Duration from two Timestamps in pseudo code. + Timestamp start = ...; + Timestamp end = ...; + Duration duration = ...; + duration.seconds = end.seconds - start.seconds; + duration.nanos = end.nanos - start.nanos; + if (duration.seconds < 0 && duration.nanos > 0) { + duration.seconds += 1; + duration.nanos -= 1000000000; + } else if (duration.seconds > 0 && duration.nanos < 0) { + duration.seconds -= 1; + duration.nanos += 1000000000; + } + Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. + Timestamp start = ...; + Duration duration = ...; + Timestamp end = ...; + end.seconds = start.seconds + duration.seconds; + end.nanos = start.nanos + duration.nanos; + if (end.nanos < 0) { + end.seconds -= 1; + end.nanos += 1000000000; + } else if (end.nanos >= 1000000000) { + end.seconds += 1; + end.nanos -= 1000000000; + } + Example 3: Compute Duration from datetime.timedelta in Python. + td = datetime.timedelta(days=3, minutes=10) + duration = Duration() + duration.FromTimedelta(td) + # JSON Mapping + In JSON format, the Duration type is encoded as a string rather than an + object, where the string ends in the suffix "s" (indicating seconds) and + is preceded by the number of seconds, with nanoseconds expressed as + fractional seconds. For example, 3 seconds with 0 nanoseconds should be + encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should + be expressed in JSON format as "3.000000001s", and 3 seconds and 1 + microsecond should be expressed in JSON format as "3.000001s". + """ input google__protobuf__DurationInput @tag(id: "google.protobuf.Duration") { + """ + Signed fractions of a second at nanosecond resolution of the span + of time. Durations less than one second are represented with a 0 + `seconds` field and a positive or negative `nanos` field. For durations + of one second or more, a non-zero value for the `nanos` field must be + of the same sign as the `seconds` field. Must be from -999,999,999 + to +999,999,999 inclusive. + """ nanos: Int + """ + Signed seconds of the span of time. Must be from -315,576,000,000 + to +315,576,000,000 inclusive. Note: these bounds are computed from: + 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + """ seconds: Int } + """ + Wrapper message for `int32`. + The JSON representation for `Int32Value` is JSON number. + """ input google__protobuf__Int32ValueInput @tag(id: "google.protobuf.Int32Value") { + """ + The int32 value. + """ value: Int } + """ + Wrapper message for `string`. + The JSON representation for `StringValue` is JSON string. + """ input google__protobuf__StringValue @tag(id: "google.protobuf.StringValue") { + """ + The string value. + """ value: String } + """ + A Timestamp represents a point in time independent of any time zone or local + calendar, encoded as a count of seconds and fractions of seconds at + nanosecond resolution. The count is relative to an epoch at UTC midnight on + January 1, 1970, in the proleptic Gregorian calendar which extends the + Gregorian calendar backwards to year one. + All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + second table is needed for interpretation, using a [24-hour linear + smear](https://developers.google.com/time/smear). + The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + restricting to that range, we ensure that we can convert to and from [RFC + 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + # Examples + Example 1: Compute Timestamp from POSIX `time()`. + Timestamp timestamp; + timestamp.set_seconds(time(NULL)); + timestamp.set_nanos(0); + Example 2: Compute Timestamp from POSIX `gettimeofday()`. + struct timeval tv; + gettimeofday(&tv, NULL); + Timestamp timestamp; + timestamp.set_seconds(tv.tv_sec); + timestamp.set_nanos(tv.tv_usec * 1000); + Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + Timestamp timestamp; + timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + long millis = System.currentTimeMillis(); + Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + .setNanos((int) ((millis % 1000) * 1000000)).build(); + Example 5: Compute Timestamp from Java `Instant.now()`. + Instant now = Instant.now(); + Timestamp timestamp = + Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + .setNanos(now.getNano()).build(); + Example 6: Compute Timestamp from current time in Python. + timestamp = Timestamp() + timestamp.GetCurrentTime() + # JSON Mapping + In JSON format, the Timestamp type is encoded as a string in the + [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + where {year} is always expressed using four digits while {month}, {day}, + {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + is required. A proto3 JSON serializer should always use UTC (as indicated by + "Z") when printing the Timestamp type and a proto3 JSON parser should be + able to accept both UTC and other timezones (as indicated by an offset). + For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + 01:30 UTC on January 15, 2017. + In JavaScript, one can convert a Date object to this format using the + standard + [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + method. In Python, a standard `datetime.datetime` object can be converted + to this format using + [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + the Joda Time's [`ISODateTimeFormat.dateTime()`]( + http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D + ) to obtain a formatter capable of generating timestamps in this format. + """ input google__protobuf__TimestampInput @tag(id: "google.protobuf.Timestamp") { + """ + Non-negative fractions of a second at nanosecond resolution. Negative + second values with fractions must still have non-negative nanos values + that count forward in time. Must be from 0 to 999,999,999 + inclusive. + """ nanos: Int + """ + Represents seconds of UTC time since Unix epoch + 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + 9999-12-31T23:59:59Z inclusive. + """ seconds: Int } + """ + movie message payload + """ input movies__MovieInput @tag(id: "movies.Movie") { + """ + list of cast + """ cast: [String] duration: google__protobuf__DurationInput genre: movies__GenreInput name: String rating: Float + """ + SubMovie reference + """ + subMovie: movies__Movie__SubMovieInput time: google__protobuf__TimestampInput year: google__protobuf__Int32ValueInput } @@ -38,47 +201,268 @@ input movies__MovieRequest @tag(id: "movies.MovieRequest") { movie: movies__MovieInput } + """ + This is a comment for submovie + """ +input movies__Movie__SubMovieInput @tag(id: "movies.Movie.SubMovie") { + """ + This is a comment for movie format in submovie + """ + format: movies__Movie__MovieFormatInput + """ + This is a comment for sub_rating + """ + subRating: Float +} + input movies__SearchByCastRequest @tag(id: "movies.SearchByCastRequest") { castName: google__protobuf__StringValue } +""" +This is a comment for Genre enum +""" enum movies__Genre { - ACTION + "" + This is a comment for DRAMA variant + "" DRAMA + "" + This is a comment for UNSPECIFIED variant + "" UNSPECIFIED + ACTION +} + +""" +Represents the format in which a movie can be released +""" +enum movies__Movie__MovieFormat { + "" + The movie is released in IMAX format + "" + IMAX } type Query { + """ + get all movies + """ movies__AnotherExample__GetMovies(movieRequest: movies__MovieRequest!): movies__MoviesResult! @grpc(body: "{{.args.movieRequest}}", method: "movies.AnotherExample.GetMovies") + """ + search movies by the name of the cast + """ movies__AnotherExample__SearchMoviesByCast(searchByCastRequest: movies__SearchByCastRequest!): movies__Movie! @grpc(body: "{{.args.searchByCastRequest}}", method: "movies.AnotherExample.SearchMoviesByCast") + """ + get all movies + """ movies__Example__GetMovies(movieRequest: movies__MovieRequest!): movies__MoviesResult! @grpc(body: "{{.args.movieRequest}}", method: "movies.Example.GetMovies") + """ + search movies by the name of the cast + """ movies__Example__SearchMoviesByCast(searchByCastRequest: movies__SearchByCastRequest!): movies__Movie! @grpc(body: "{{.args.searchByCastRequest}}", method: "movies.Example.SearchMoviesByCast") } + """ + A Duration represents a signed, fixed-length span of time represented + as a count of seconds and fractions of seconds at nanosecond + resolution. It is independent of any calendar and concepts like "day" + or "month". It is related to Timestamp in that the difference between + two Timestamp values is a Duration and it can be added or subtracted + from a Timestamp. Range is approximately +-10,000 years. + # Examples + Example 1: Compute Duration from two Timestamps in pseudo code. + Timestamp start = ...; + Timestamp end = ...; + Duration duration = ...; + duration.seconds = end.seconds - start.seconds; + duration.nanos = end.nanos - start.nanos; + if (duration.seconds < 0 && duration.nanos > 0) { + duration.seconds += 1; + duration.nanos -= 1000000000; + } else if (duration.seconds > 0 && duration.nanos < 0) { + duration.seconds -= 1; + duration.nanos += 1000000000; + } + Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. + Timestamp start = ...; + Duration duration = ...; + Timestamp end = ...; + end.seconds = start.seconds + duration.seconds; + end.nanos = start.nanos + duration.nanos; + if (end.nanos < 0) { + end.seconds -= 1; + end.nanos += 1000000000; + } else if (end.nanos >= 1000000000) { + end.seconds += 1; + end.nanos -= 1000000000; + } + Example 3: Compute Duration from datetime.timedelta in Python. + td = datetime.timedelta(days=3, minutes=10) + duration = Duration() + duration.FromTimedelta(td) + # JSON Mapping + In JSON format, the Duration type is encoded as a string rather than an + object, where the string ends in the suffix "s" (indicating seconds) and + is preceded by the number of seconds, with nanoseconds expressed as + fractional seconds. For example, 3 seconds with 0 nanoseconds should be + encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should + be expressed in JSON format as "3.000000001s", and 3 seconds and 1 + microsecond should be expressed in JSON format as "3.000001s". + """ type google__protobuf__Duration @tag(id: "google.protobuf.Duration") { + """ + Signed fractions of a second at nanosecond resolution of the span + of time. Durations less than one second are represented with a 0 + `seconds` field and a positive or negative `nanos` field. For durations + of one second or more, a non-zero value for the `nanos` field must be + of the same sign as the `seconds` field. Must be from -999,999,999 + to +999,999,999 inclusive. + """ nanos: Int + """ + Signed seconds of the span of time. Must be from -315,576,000,000 + to +315,576,000,000 inclusive. Note: these bounds are computed from: + 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + """ seconds: Int } + """ + Wrapper message for `int32`. + The JSON representation for `Int32Value` is JSON number. + """ type google__protobuf__Int32Value @tag(id: "google.protobuf.Int32Value") { + """ + The int32 value. + """ value: Int } + """ + A Timestamp represents a point in time independent of any time zone or local + calendar, encoded as a count of seconds and fractions of seconds at + nanosecond resolution. The count is relative to an epoch at UTC midnight on + January 1, 1970, in the proleptic Gregorian calendar which extends the + Gregorian calendar backwards to year one. + All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + second table is needed for interpretation, using a [24-hour linear + smear](https://developers.google.com/time/smear). + The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + restricting to that range, we ensure that we can convert to and from [RFC + 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + # Examples + Example 1: Compute Timestamp from POSIX `time()`. + Timestamp timestamp; + timestamp.set_seconds(time(NULL)); + timestamp.set_nanos(0); + Example 2: Compute Timestamp from POSIX `gettimeofday()`. + struct timeval tv; + gettimeofday(&tv, NULL); + Timestamp timestamp; + timestamp.set_seconds(tv.tv_sec); + timestamp.set_nanos(tv.tv_usec * 1000); + Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + Timestamp timestamp; + timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + long millis = System.currentTimeMillis(); + Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + .setNanos((int) ((millis % 1000) * 1000000)).build(); + Example 5: Compute Timestamp from Java `Instant.now()`. + Instant now = Instant.now(); + Timestamp timestamp = + Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + .setNanos(now.getNano()).build(); + Example 6: Compute Timestamp from current time in Python. + timestamp = Timestamp() + timestamp.GetCurrentTime() + # JSON Mapping + In JSON format, the Timestamp type is encoded as a string in the + [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + where {year} is always expressed using four digits while {month}, {day}, + {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + is required. A proto3 JSON serializer should always use UTC (as indicated by + "Z") when printing the Timestamp type and a proto3 JSON parser should be + able to accept both UTC and other timezones (as indicated by an offset). + For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + 01:30 UTC on January 15, 2017. + In JavaScript, one can convert a Date object to this format using the + standard + [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + method. In Python, a standard `datetime.datetime` object can be converted + to this format using + [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + the Joda Time's [`ISODateTimeFormat.dateTime()`]( + http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D + ) to obtain a formatter capable of generating timestamps in this format. + """ type google__protobuf__Timestamp @tag(id: "google.protobuf.Timestamp") { + """ + Non-negative fractions of a second at nanosecond resolution. Negative + second values with fractions must still have non-negative nanos values + that count forward in time. Must be from 0 to 999,999,999 + inclusive. + """ nanos: Int + """ + Represents seconds of UTC time since Unix epoch + 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + 9999-12-31T23:59:59Z inclusive. + """ seconds: Int } + """ + movie message payload + """ type movies__Movie @tag(id: "movies.Movie") { + """ + list of cast + """ cast: [String] duration: google__protobuf__Duration genre: movies__Genre name: String rating: Float + """ + SubMovie reference + """ + subMovie: movies__Movie__SubMovie time: google__protobuf__Timestamp year: google__protobuf__Int32Value } + """ + This is a comment for submovie + """ +type movies__Movie__SubMovie @tag(id: "movies.Movie.SubMovie") { + """ + This is a comment for movie format in submovie + """ + format: movies__Movie__MovieFormat + """ + This is a comment for sub_rating + """ + subRating: Float +} + + """ + movie result message, contains list of movies + """ type movies__MoviesResult @tag(id: "movies.MoviesResult") { + """ + list of movies + """ result: [movies__Movie] } diff --git a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__required_types.snap b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__required_types.snap index e3d4e38935..dbecc6db3e 100644 --- a/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__required_types.snap +++ b/src/core/generator/snapshots/tailcall__core__generator__from_proto__test__required_types.snap @@ -10,6 +10,9 @@ type Query { person__PersonService__GetPerson: person__Person! @grpc(method: "person.PersonService.GetPerson") } + """ + Defines a person + """ type person__Person @tag(id: "person.Person") { email: String id: Int! @@ -18,6 +21,9 @@ type person__Person @tag(id: "person.Person") { stringMap: JSON } + """ + Defines a phone number + """ type person__PhoneNumber @tag(id: "person.PhoneNumber") { number: String! type: String diff --git a/tailcall-fixtures/fixtures/protobuf/movies.proto b/tailcall-fixtures/fixtures/protobuf/movies.proto index 4b6baa2802..382adabb1f 100644 --- a/tailcall-fixtures/fixtures/protobuf/movies.proto +++ b/tailcall-fixtures/fixtures/protobuf/movies.proto @@ -6,9 +6,12 @@ import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; import "google/protobuf/duration.proto"; +// This is a comment for Genre enum enum Genre { + // This is a comment for UNSPECIFIED variant UNSPECIFIED = 0; ACTION = 1; + // This is a comment for DRAMA variant DRAMA = 2; } @@ -27,6 +30,25 @@ message Movie { google.protobuf.Timestamp time = 5; Genre genre = 6; google.protobuf.Duration duration = 7; + + // Represents the format in which a movie can be released + enum MovieFormat { + // The movie is released in IMAX format + IMAX = 0; + } + + /** + * SubMovie reference + */ + SubMovie sub_movie = 8; + + // This is a comment for submovie + message SubMovie { + // This is a comment for sub_rating + float sub_rating = 1; + // This is a comment for movie format in submovie + MovieFormat format = 2; + } } message EmptyRequest {} diff --git a/tests/execution/test-scalars-validation.md b/tests/execution/test-scalars-validation.md index 1c70a0b3e0..2abed70b19 100644 --- a/tests/execution/test-scalars-validation.md +++ b/tests/execution/test-scalars-validation.md @@ -16,10 +16,10 @@ type Query { - method: POST url: http://localhost:8000/graphql body: - query: '{ emailInput(x: 123) }' + query: "{ emailInput(x: 123) }" - method: POST url: http://localhost:8000/graphql body: - query: '{ emailOutput }' + query: "{ emailOutput }" ```