tinc/lib.rs
1//! Tinc is a GRPc to REST transcoder which generates axum routes for services defined in proto3 files.
2//!
3//! To use this crate check out [tinc-build](https://docs.rs/tinc_build) refer to the [`annotations.proto`](Annotations)
4#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
5#![cfg_attr(feature = "docs", doc = "## Feature flags")]
6#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
7//! ## Examples
8//!
9//! ```protobuf
10//! service SimpleService {
11//! rpc Ping(PingRequest) returns (PingResponse) {
12//! option (tinc.method).endpoint = {
13//! post: "/ping"
14//! };
15//! option (tinc.method).endpoint = {
16//! get: "/ping/{arg}"
17//! };
18//! }
19//! }
20//!
21//! message PingRequest {
22//! string arg = 1;
23//! }
24//!
25//! message PingResponse {
26//! string result = 1;
27//! }
28//! ```
29//!
30//! You can also change the serialization / deserialization of the messages in json by annotating stuff like
31//!
32//! ```protobuf
33//! message FlattenedMessage {
34//! SomeOtherMessage some_other = 1 [(tinc.field) = {
35//! flatten: true
36//! }];
37//! }
38//!
39//! message SomeOtherMessage {
40//! string name = 1 [(tinc.field).rename = "NAME"];
41//! int32 id = 2 [(tinc.field).visibility = OUTPUT_ONLY];
42//! int32 age = 3;
43//!
44//! message NestedMessage {
45//! int32 depth = 1;
46//! }
47//!
48//! NestedMessage nested = 4 [(tinc.field) = {
49//! flatten: true
50//! }];
51//! SomeOtherMessage2 address = 5 [(tinc.field) = {
52//! flatten: true
53//! }];
54//! }
55//!
56//! message SomeOtherMessage2 {
57//! string house_number = 1;
58//! string street = 2;
59//! string city = 3;
60//! string state = 4;
61//! string zip_code = 5;
62//! }
63//! ```
64//!
65//! Tinc also has a fully customizable CEL-based expression system which allows you to validate inputs on both GRPc / REST. Similar to <https://github.com/bufbuild/protovalidate>.
66//! Except we compile the CEL-expressions directly into rust syntax and do not ship a interpreter for runtime.
67//!
68//! For example you can do something like this
69//! ```protobuf
70//! message TestRequest {
71//! string name = 1 [(tinc.field).constraint.string = {
72//! min_len: 1,
73//! max_len: 10,
74//! }];
75//! map<string, int32> things = 2 [(tinc.field).constraint.map = {
76//! key: {
77//! string: {
78//! min_len: 1,
79//! max_len: 10,
80//! }
81//! }
82//! value: {
83//! int32: {
84//! gte: 0,
85//! lte: 100,
86//! }
87//! }
88//! }];
89//! }
90//! ```
91//!
92//! Then every message that goes into your service handler will be validated and all validation errors will be returned to the user (either via json for http or protobuf for grpc)
93//!
94//! ```json
95//! {
96//! "name": "troy",
97//! "things": {
98//! "thing1": "1000",
99//! "thing2": 42000
100//! }
101//! }
102//! ```
103//!
104//! returns this:
105//!
106//! ```json
107//! {
108//! "code": 3,
109//! "details": {
110//! "request": {
111//! "violations": [
112//! {
113//! "description": "invalid type: string \"1000\", expected i32 at line 4 column 24",
114//! "field": "things[\"thing1\"]"
115//! },
116//! {
117//! "description": "value must be less than or equal to `100`",
118//! "field": "things[\"thing2\"]"
119//! }
120//! ]
121//! }
122//! },
123//! "message": "bad request"
124//! }
125//! ```
126//!
127//! The cel expressions can be extended to provide custom expressions:
128//!
129//! ```protobuf
130//! message TestRequest {
131//! // define a custom expression specifically for this field
132//! string name = 1 [(tinc.field).constraint.cel = {
133//! expression: "input == 'troy'"
134//! message: "must equal `troy` but got `{input}`"
135//! }];
136//! }
137//!
138//! // --- or ---
139//!
140//! extend google.protobuf.FieldOptions {
141//! // define a custom option that can be applied to multiple fields.
142//! string must_eq = 10200 [(tinc.predefined) = {
143//! expression: "input == this"
144//! message: "must equal `{this}` but got `{input}`"
145//! }];
146//! }
147//!
148//! message TestRequest {
149//! // apply said option to this field.
150//! string name = 1 [must_eq = "troy"];
151//! }
152//! ```
153//!
154//! ```json
155//! {
156//! "code": 3,
157//! "details": {
158//! "request": {
159//! "violations": [
160//! {
161//! "description": "must equal `troy` but got `notTroy`",
162//! "field": "name"
163//! }
164//! ]
165//! }
166//! },
167//! "message": "bad request"
168//! }
169//! ```
170//!
171//! ## What is supported
172//!
173//! - [x] Endpoint path parameters with nested keys
174//! - [x] Mapped response bodies to a specific field
175//! - [x] Binary request/response bodies.
176//! - [x] Query string parsing
177//! - [x] Custom validation expressions, including validation on unary and streaming.
178//! - [x] OpenAPI 3.1 Spec Generation
179//! - [ ] Documentation
180//! - [ ] Tests
181//! - [ ] REST streaming
182//! - [ ] Multipart forms
183//!
184//! ## Choices made
185//!
186//! 1. Use a custom proto definition for the proto schema instead of using [google predefined ones](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto).
187//!
188//! The reasoning is because we wanted to support additional features that google did not have, we can add a compatibility layer to convert from google to our version if we want in the future. Such as CEL based validation, openapi schema, json flatten / tagged oneofs.
189//!
190//! 2. Non-proto3-optional fields are required for JSON.
191//!
192//! If a field is not marked as `optional` then it is required by default and not providing it will result in an error returned during deserialization. You can opt-out of this behaviour using `[(tinc.field).json_omittable = TRUE]` which will make it so if the value is not provided it will use the default value (same behaviour as protobuf)`. The rationale behind this is from the way REST apis are typically used. Normally you provide all the fields you want and you do not have default values for rest APIs. So allowing fields to be defaulted may cause some issues related to people not providing required fields but the default value is a valid value for that field and then the endpoint misbehaves.
193//!
194//! 3. Stop on last error.
195//!
196//! Typically when using serde we stop on the first error. We believe that makes errors less valuable since we only ever get the first error that occurred in the stream instead of every error we had. There are some libraries that aim to solve this issue such as [`eserde`](https://lib.rs/crates/eserde) however we opted to build our solution fully custom since their's have quite a few drawbacks and we (at compile time) know the full structure since its defined in the protobuf schema, allowing us to generate better code for the deserialization process and store errors more effectively without introducing much/any runtime overhead.
197//!
198//! ## Alternatives to this
199//!
200//! ### 1. [GRPc-Gateway](https://grpc-ecosystem.github.io/grpc-gateway/)
201//!
202//! GRPc-Gateway is the most popular way of converting from GRPc endpoint to rest endpoints using google's protoschema for doing so. The reason I dont like grpc-gateway stems from 2 things:
203//!
204//! 1. grpc gateway requires a reverse proxy or external service which does the transcoding and then forwards you http requests.
205//! 2. You do not have any control over how the json is structured. It uses protobuf-json schema encoding.
206//!
207//! ### 2. [GRPc-Web](https://github.com/grpc/grpc-web)
208//!
209//! GRPc-Web is a browser compatible version of the grpc spec. This is good for maintaining a single api across browsers / servers, but if you still want a rest API for your service it does not help with that.
210//!
211//! ## License
212//!
213//! This project is licensed under the MIT or Apache-2.0 license.
214//! You can choose between one of them if you use this work.
215//!
216//! `SPDX-License-Identifier: MIT OR Apache-2.0`
217#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
218#![cfg_attr(docsrs, feature(doc_auto_cfg))]
219#![deny(missing_docs)]
220#![deny(unreachable_pub)]
221#![deny(clippy::undocumented_unsafe_blocks)]
222#![deny(clippy::multiple_unsafe_ops_per_block)]
223
224#[doc(hidden)]
225pub mod reexports {
226 #[cfg(feature = "tonic")]
227 pub use tonic;
228 pub use {axum, bytes, chrono, http, linkme, mediatype, regex, serde, serde_derive, serde_json, serde_repr};
229 #[cfg(feature = "prost")]
230 pub use {prost, prost_types};
231}
232
233#[doc(hidden)]
234#[path = "private/mod.rs"]
235pub mod __private;
236
237pub mod well_known;
238
239pub use openapiv3_1 as openapi;
240
241/// TincServices are typically generated by the `tinc-build`
242/// crate and this trait lets you convert the service
243/// into an axum router.
244pub trait TincService {
245 /// Convert the service into an axum router.
246 fn into_router(self) -> axum::Router;
247
248 /// Get the raw openapi spec for this tinc service.
249 fn openapi_schema_str(&self) -> &'static str;
250
251 /// Get the openapi spec for this service.
252 fn openapi_schema(&self) -> openapiv3_1::OpenApi {
253 serde_json::from_str(self.openapi_schema_str()).expect("invalid openapi schema")
254 }
255}
256
257/// Include the proto by specifying the package.
258#[macro_export]
259macro_rules! include_proto {
260 ($package:tt) => {
261 include!(concat!(env!("OUT_DIR"), concat!("/", $package, ".rs")));
262 };
263}
264
265/// Changelogs generated by [scuffle_changelog]
266#[cfg(feature = "docs")]
267#[scuffle_changelog::changelog]
268pub mod changelog {}
269
270/// [`annotations.proto`](https://github.com/ScuffleCloud/scuffle/blob/main/crates/tinc/annotations.proto)
271#[cfg(feature = "docs")]
272#[doc = concat!("```protobuf\n", include_str!("../annotations.proto") ,"```")]
273pub type Annotations = ();