conjure_codegen/
lib.rs

1// Copyright 2018 Palantir Technologies, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Code generation for Conjure definitions.
15//!
16//! # Examples
17//!
18//! Code generation via a build script, assuming we have a `service-api.conjure.json` file in the crate root:
19//!
20//! build.rs:
21//!
22//! ```no_run
23//! use std::env;
24//! use std::path::Path;
25//!
26//! fn main() {
27//!     let input = "service-api.conjure.json";
28//!     let output = Path::new(&env::var_os("OUT_DIR").unwrap()).join("service_api");
29//!
30//!     println!("cargo:rerun-if-changed={}", input);
31//!     conjure_codegen::Config::new()
32//!         .run_rustfmt(false)
33//!         .strip_prefix("com.foobar.service".to_string())
34//!         .generate_files(input, output)
35//!         .unwrap();
36//! }
37//! ```
38//!
39//! src/lib.rs:
40//!
41//! ```ignore
42//! mod service_api {
43//!     include!(concat!(env!("OUT_DIR"), "/service_api/mod.rs"));
44//! }
45//! ```
46//!
47//! # Types
48//!
49//! ## Builtin
50//!
51//! Builtin types map directly to existing Rust types:
52//!
53//! | Conjure       | Rust                                 |
54//! | ------------- | ------------------------------------ |
55//! | `string`      | `String`                             |
56//! | `datetime`    | `chrono::DateTime<Utc>`              |
57//! | `integer`     | `i32`                                |
58//! | `double`      | `f64`                                |
59//! | `safelong`    | `conjure_object::SafeLong`           |
60//! | `binary`      | `bytes::Bytes`                       |
61//! | `any`         | `conjure_object::Any`                |
62//! | `boolean`     | `bool`                               |
63//! | `uuid`        | `uuid::Uuid`                         |
64//! | `rid`         | `conjure_object::ResourceIdentifier` |
65//! | `bearertoken` | `conjure_object::BearerToken`        |
66//! | `optional<T>` | `Option<T>`                          |
67//! | `list<T>`     | `Vec<T>`                             |
68//! | `set<T>`      | `BTreeSet<T>`                        |
69//! | `map<K, V>`   | `BTreeMap<K, V>`                     |
70//!
71//! Many of these are exposed by the `conjure-object` crate, which is a required dependency of crates containing the
72//! generated code.
73//!
74//! ### `double`
75//!
76//! Rust's `f64` type does not implement `Ord`, `Eq`, or `Hash`, which requires some special casing. Sets and maps keyed
77//! directly by `double` are represented as `BTreeSet<DoubleKey>` and `BTreeMap<DoubleKey, T>`, where the
78//! [`DoubleKey`](conjure_object::DoubleKey) type wraps `f64` and implements those traits by ordering `NaN` greater
79//! than all other values and equal to itself.
80//!
81//! Conjure aliases, objects, and unions wrapping `double` types have trait implementations which use the same logic.
82//!
83//! ## Objects
84//!
85//! Conjure objects turn into Rust structs along with builders used to construct them:
86//!
87//! ```
88//! # use conjure_codegen::example_types::product::{ManyFieldExample, StringAliasExample};
89//! let object = ManyFieldExample::builder()
90//!     .string("foo")
91//!     .integer(123)
92//!     .double_value(3.14)
93//!     .alias(StringAliasExample("foobar".to_string()))
94//!     .optional_item("bar".to_string())
95//!     .items(vec!["hello".to_string(), "world".to_string()])
96//!     .build();
97//!
98//! assert_eq!(object.string(), "foo");
99//! assert_eq!(object.optional_item(), Some("bar"));
100//! ```
101//!
102//! The builder types are by-value and infallible - the compiler will prevent code from compiling if all required
103//! fields are not set. The API requires that all required fields be set first strictly in declaration order,
104//! after which optional fields can be set in any order.
105//!
106//! Objects with 3 or fewer required fields also have an explicit constructor:
107//!
108//! ```rust
109//! # use conjure_codegen::example_types::product::BooleanExample;
110//! let object = BooleanExample::new(true);
111//!
112//! assert_eq!(object.coin(), true);
113//! ```
114//!
115//! The generated structs implement `Debug`, `Clone`, `PartialEq`, Eq, `PartialOrd`, `Ord`, `Hash`, `Serialize`, and
116//! `Deserialize`. They implement `Copy` if they consist entirely of copyable primitive types.
117//!
118//! ## Unions
119//!
120//! Conjure unions turn into Rust enums. By default, unions are *extensible* through an additional `Unknown` variant.
121//! This allows unions to be forward-compatible by allowing clients to deserialize variants they don't yet know about
122//! and reserialize them properly:
123//!
124//! ```
125//! # use conjure_codegen::example_types::product::UnionTypeExample;
126//! # let union_value = UnionTypeExample::If(0);
127//! match union_value {
128//!     UnionTypeExample::StringExample(string) => {
129//!         // ...
130//!     }
131//!     UnionTypeExample::Set(set) => {
132//!         // ...
133//!     }
134//!     // ...
135//!     UnionTypeExample::Unknown(unknown) => {
136//!         println!("got unknown variant: {}", unknown.type_());
137//!     }
138//!     # _ => {}
139//! }
140//! ```
141//!
142//! The generated enums implement `Debug`, `Clone`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`, `Serialize`, and
143//! `Deserialize`. Union variants which are themselves unions are boxed in the generated enum to avoid self-referential
144//! type definitions.
145//!
146//! ## Enums
147//!
148//! Conjure enums turn into Rust enums. By default, enums are *extensible*. This allows enums to be forward-compatible
149//! by allowing clients to deserialize variants they don't yet know about and reserialize them properly:
150//!
151//! ```
152//! # use conjure_codegen::example_types::product::EnumExample;
153//! # let enum_value = EnumExample::One;
154//! match enum_value {
155//!     EnumExample::One => println!("found one"),
156//!     EnumExample::Two => println!("found two"),
157//!     EnumExample::Unknown(unknown) => println!("got unknown variant: {}", unknown),
158//! }
159//! ```
160//!
161//! The generated enums implement `Debug`, `Clone`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`, `Display`,
162//! `Serialize`, and `Deserialize`.
163//!
164//! ## Aliases
165//!
166//! Conjure aliases turn into Rust newtype structs that act like their inner value:
167//!
168//! ```
169//! # use conjure_codegen::example_types::product::StringAliasExample;
170//! let alias_value = StringAliasExample("hello world".to_string());
171//! assert!(alias_value.starts_with("hello"));
172//! ```
173//!
174//! The generated structs implement `Deref`, `DerefMut`, `Debug`, `Clone`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`,
175//! `Hash`, `Serialize`, and `Deserialize`. They implement `Copy` if they wrap a copyable primitive type, `Default`
176//! if they wrap a type implementing `Default`, `FromIterator` if they wrap a type implementing `FromIterator`, and
177//! `Display` if they wrap a type implementing `Display`. They also implement `From<T>`, where `T` is the fully
178//! de-aliased inner type.
179//!
180//! ## Errors
181//!
182//! Conjure errors turn into Rust structs storing the error's parameters as if it were a Conjure object. The struct
183//! additionally implements the `conjure_error::ErrorType` trait which encodes the extra error metadata:
184//!
185//! ```
186//! # use conjure_codegen::example_types::product::InvalidServiceDefinition;
187//! # let (name, definition) = ("", "");
188//! use conjure_error::{ErrorType, ErrorCode};
189//!
190//! let error = InvalidServiceDefinition::new(name, definition);
191//!
192//! assert_eq!(error.code(), ErrorCode::InvalidArgument);
193//! assert_eq!(error.name(), "Conjure:InvalidServiceDefinition");
194//! ```
195//!
196//! ## Services
197//!
198//! Conjure services turn into client- and server-side interfaces:
199//!
200//! ### Clients
201//!
202//! The client object wraps a raw HTTP client and provides methods to interact with the service's endpoints. Both
203//! synchronous and asynchronous clients are provided:
204//!
205//! Synchronous:
206//! ```
207//! use conjure_http::client::Service;
208//! # use conjure_codegen::example_types::another::TestServiceClient;
209//! # fn foo<T: conjure_http::client::Client>(http_client: T) -> Result<(), conjure_error::Error> {
210//! # let auth_token = "foobar".parse().unwrap();
211//! let client = TestServiceClient::new(http_client);
212//! let file_systems = client.get_file_systems(&auth_token)?;
213//! # Ok(())
214//! # }
215//! ```
216//!
217//! Asynchronous:
218//! ```
219//! use conjure_http::client::AsyncService;
220//! # use conjure_codegen::example_types::another::TestServiceAsyncClient;
221//! # async fn foo<T: conjure_http::client::AsyncClient>(http_client: T) -> Result<(), conjure_error::Error> {
222//! # let auth_token = "foobar".parse().unwrap();
223//! let client = TestServiceAsyncClient::new(http_client);
224//! let file_systems = client.get_file_systems(&auth_token).await?;
225//! # Ok(())
226//! # }
227//! ```
228//!
229//! ### Servers
230//!
231//! Conjure generates a trait and accompanying wrapper resource which are used to implement the service's endpoints.
232//! Both synchronous and asynchronous servers are supported:
233//!
234//! Synchronous:
235//! ```ignore
236//! struct TestServiceHandler;
237//!
238//! impl<T> TestService<T> for TestServiceHandler
239//! where
240//!     T: Read
241//! {
242//!     fn get_file_systems(
243//!         &self,
244//!         auth: AuthToken,
245//!     ) -> Result<BTreeMap<String, BackingFileSystem>, Error> {
246//!         // ...
247//!     }
248//!
249//!     // ...
250//! }
251//!
252//! let resource = TestServiceResource::new(TestServiceHandler);
253//! http_server.register(resource);
254//! ```
255//!
256//! Asynchronous:
257//! ```ignore
258//! struct TestServiceHandler;
259//!
260//! impl<T> AsyncTestService<T> for TestServiceHandler
261//! where
262//!     T: AsyncRead + 'static + Send
263//! {
264//!     async fn get_file_systems(
265//!         &self,
266//!         auth: AuthToken,
267//!     ) -> Result<BTreeMap<String, BackingFileSystem>, Error> {
268//!         // ...
269//!     }
270//!
271//!     // ...
272//! }
273//!
274//! let resource = TestServiceResource::new(TestServiceHandler);
275//! http_server.register(resource);
276//! ```
277//!
278//! ### Endpoint Tags
279//!
280//! * `server-request-context` - The generated server trait method will have an additional
281//!   `RequestContext` argument providing lower level access to request and response information.
282#![warn(clippy::all, missing_docs)]
283#![allow(clippy::needless_doctest_main)]
284#![recursion_limit = "256"]
285
286use crate::context::Context;
287use crate::merge_toml::left_merge;
288use crate::types::{ConjureDefinition, TypeDefinition};
289use anyhow::{bail, Context as _, Error};
290use proc_macro2::TokenStream;
291use quote::quote;
292use std::collections::BTreeMap;
293use std::env;
294use std::ffi::OsStr;
295use std::fs;
296use std::path::Path;
297use toml::Value;
298
299mod aliases;
300mod cargo_toml;
301mod clients;
302mod context;
303mod enums;
304mod errors;
305mod http_paths;
306mod merge_toml;
307mod objects;
308mod servers;
309#[allow(dead_code, clippy::all)]
310#[rustfmt::skip]
311mod types;
312mod human_size;
313mod unions;
314
315/// Examples of generated Conjure code.
316///
317/// This module is only intended to be present in documentation; it shouldn't be relied on by any library code.
318#[cfg(feature = "example-types")]
319#[allow(warnings)]
320#[rustfmt::skip]
321pub mod example_types;
322
323struct CrateInfo {
324    name: String,
325    version: String,
326}
327
328/// Codegen configuration.
329pub struct Config {
330    exhaustive: bool,
331    serialize_empty_collections: bool,
332    strip_prefix: Option<String>,
333    version: Option<String>,
334    build_crate: Option<CrateInfo>,
335    extra_manifest_config: Option<Value>,
336}
337
338impl Default for Config {
339    fn default() -> Config {
340        Config::new()
341    }
342}
343
344impl Config {
345    /// Creates a new `Config` with default settings.
346    pub fn new() -> Config {
347        Config {
348            exhaustive: false,
349            serialize_empty_collections: false,
350            strip_prefix: None,
351            version: None,
352            build_crate: None,
353            extra_manifest_config: None,
354        }
355    }
356
357    /// Controls exhaustive matchability of unions and enums.
358    ///
359    /// Non-exhaustive unions and enums have the ability to deserialize and reserialize unknown variants. This enables
360    /// clients to be more forward-compatible with changes made by newer servers.
361    ///
362    /// Defaults to `false`.
363    pub fn exhaustive(&mut self, exhaustive: bool) -> &mut Config {
364        self.exhaustive = exhaustive;
365        self
366    }
367
368    /// Controls serialization of empty collection fields in objects.
369    ///
370    /// Some Conjure implementations don't properly handle deserialization of objects when empty collections are
371    /// omitted. Enabling this option will cause empty optional, set, list, and map fields to be included in the
372    /// serialized output.
373    pub fn serialize_empty_collections(
374        &mut self,
375        serialize_empty_collections: bool,
376    ) -> &mut Config {
377        self.serialize_empty_collections = serialize_empty_collections;
378        self
379    }
380
381    /// No longer used.
382    #[deprecated(note = "no longer used", since = "1.2.0")]
383    pub fn run_rustfmt(&mut self, _run_rustfmt: bool) -> &mut Config {
384        self
385    }
386
387    /// No longer used.
388    #[deprecated(note = "no longer used", since = "1.2.0")]
389    pub fn rustfmt<T>(&mut self, _rustfmt: T) -> &mut Config
390    where
391        T: AsRef<OsStr>,
392    {
393        self
394    }
395
396    /// Sets a prefix that will be stripped from package names.
397    ///
398    /// Defaults to `None`.
399    pub fn strip_prefix<T>(&mut self, strip_prefix: T) -> &mut Config
400    where
401        T: Into<Option<String>>,
402    {
403        self.strip_prefix = strip_prefix.into();
404        self
405    }
406
407    /// Sets the version included in endpoint metadata for generated client bindings.
408    ///
409    /// Defaults to the version passed to [`Self::build_crate`], or `None` otherwise.
410    pub fn version<T>(&mut self, version: T) -> &mut Config
411    where
412        T: Into<Option<String>>,
413    {
414        self.version = version.into();
415        self
416    }
417
418    /// Switches generation to create a full crate.
419    ///
420    /// Defaults to just generating a single module.
421    pub fn build_crate(&mut self, name: &str, version: &str) -> &mut Config {
422        self.build_crate = Some(CrateInfo {
423            name: name.to_string(),
424            version: version.to_string(),
425        });
426        self
427    }
428
429    /// Sets extra manifest configuration to be merged into the generated Cargo.toml.
430    ///
431    /// Defaults to `None`
432    pub fn extra_manifest_config<T>(&mut self, config: T) -> &mut Config
433    where
434        T: Into<Option<Value>>,
435    {
436        self.extra_manifest_config = config.into();
437        self
438    }
439
440    /// Generates Rust source files from a JSON-encoded Conjure IR file.
441    pub fn generate_files<P, Q>(&self, ir_file: P, out_dir: Q) -> Result<(), Error>
442    where
443        P: AsRef<Path>,
444        Q: AsRef<Path>,
445    {
446        self.generate_files_inner(ir_file.as_ref(), out_dir.as_ref())
447    }
448
449    fn generate_files_inner(&self, ir_file: &Path, out_dir: &Path) -> Result<(), Error> {
450        let defs = self.parse_ir(ir_file)?;
451
452        if defs.version() != 1 {
453            bail!("unsupported IR version {}", defs.version());
454        }
455
456        let modules = self.create_modules(&defs);
457        let (src_dir, lib_root) = if self.build_crate.is_some() {
458            (out_dir.join("src"), true)
459        } else {
460            (out_dir.to_path_buf(), false)
461        };
462
463        if let Some(info) = &self.build_crate {
464            self.write_cargo_toml(out_dir, info, &defs)?;
465            self.write_rustfmt_toml(out_dir)?;
466        }
467
468        modules.render(&src_dir, lib_root)?;
469
470        Ok(())
471    }
472
473    fn parse_ir(&self, ir_file: &Path) -> Result<ConjureDefinition, Error> {
474        let ir = fs::read_to_string(ir_file)
475            .with_context(|| format!("error reading file {}", ir_file.display()))?;
476
477        let defs = conjure_serde::json::client_from_str(&ir)
478            .with_context(|| format!("error parsing Conjure IR file {}", ir_file.display()))?;
479
480        Ok(defs)
481    }
482
483    fn create_modules(&self, defs: &ConjureDefinition) -> ModuleTrie {
484        let context = Context::new(
485            defs,
486            self.exhaustive,
487            self.serialize_empty_collections,
488            self.strip_prefix.as_deref(),
489            self.version
490                .as_deref()
491                .or_else(|| self.build_crate.as_ref().map(|v| &*v.version)),
492        );
493
494        let mut root = ModuleTrie::new();
495
496        for def in defs.types() {
497            let (type_name, contents) = match def {
498                TypeDefinition::Enum(def) => (def.type_name(), enums::generate(&context, def)),
499                TypeDefinition::Alias(def) => (def.type_name(), aliases::generate(&context, def)),
500                TypeDefinition::Union(def) => (def.type_name(), unions::generate(&context, def)),
501                TypeDefinition::Object(def) => (def.type_name(), objects::generate(&context, def)),
502            };
503
504            let type_ = Type {
505                module_name: context.module_name(type_name),
506                type_names: vec![context.type_name(type_name.name()).to_string()],
507                contents,
508            };
509            root.insert(&context.module_path(type_name), type_);
510        }
511
512        for def in defs.errors() {
513            let type_ = Type {
514                module_name: context.module_name(def.error_name()),
515                type_names: vec![context.type_name(def.error_name().name()).to_string()],
516                contents: errors::generate(&context, def),
517            };
518            root.insert(&context.module_path(def.error_name()), type_);
519        }
520
521        for def in defs.services() {
522            let client = clients::generate(&context, def);
523            let server = servers::generate(&context, def);
524
525            let contents = quote! {
526                #client
527                #server
528            };
529            let type_ = Type {
530                module_name: context.module_name(def.service_name()),
531                type_names: vec![
532                    format!("{}Client", def.service_name().name()),
533                    format!("{}AsyncClient", def.service_name().name()),
534                    context.type_name(def.service_name().name()).to_string(),
535                    format!("Async{}", def.service_name().name()),
536                    format!("{}Endpoints", def.service_name().name()),
537                    format!("Async{}Endpoints", def.service_name().name()),
538                ],
539                contents,
540            };
541            root.insert(&context.module_path(def.service_name()), type_);
542        }
543
544        root
545    }
546
547    fn write_cargo_toml(
548        &self,
549        dir: &Path,
550        info: &CrateInfo,
551        def: &ConjureDefinition,
552    ) -> Result<(), Error> {
553        fs::create_dir_all(dir)
554            .with_context(|| format!("error creating directory {}", dir.display()))?;
555
556        let metadata = def
557            .extensions()
558            .get("recommended-product-dependencies")
559            .map(|deps| cargo_toml::Metadata {
560                sls: cargo_toml::Sls {
561                    recommended_product_dependencies: deps,
562                },
563            });
564
565        let mut needs_object = false;
566        let mut needs_error = false;
567        let mut needs_http = false;
568
569        if !def.types().is_empty() {
570            needs_object = true;
571        }
572
573        if !def.errors().is_empty() {
574            needs_object = true;
575            needs_error = true;
576        }
577
578        if !def.services().is_empty() {
579            needs_http = true;
580            needs_object = true;
581        }
582
583        let conjure_version = env!("CARGO_PKG_VERSION");
584        let mut dependencies = BTreeMap::new();
585        if needs_object {
586            dependencies.insert("conjure-object", conjure_version);
587        }
588        if needs_error {
589            dependencies.insert("conjure-error", conjure_version);
590        }
591        if needs_http {
592            dependencies.insert("conjure-http", conjure_version);
593        }
594
595        let manifest = cargo_toml::Manifest {
596            package: cargo_toml::Package {
597                name: &info.name,
598                version: &info.version,
599                edition: "2018",
600                metadata,
601            },
602            dependencies,
603        };
604
605        let manifest = if let Some(extra_manifest_toml) = self.extra_manifest_config.as_ref() {
606            let mut manifest_toml = toml::Value::try_from(&manifest)?;
607            left_merge(&mut manifest_toml, extra_manifest_toml)?;
608            toml::to_string_pretty(&manifest_toml).unwrap()
609        } else {
610            toml::to_string_pretty(&manifest).unwrap()
611        };
612
613        let file = dir.join("Cargo.toml");
614
615        fs::write(&file, manifest)
616            .with_context(|| format!("error writing manifest file {}", file.display()))?;
617
618        Ok(())
619    }
620
621    fn write_rustfmt_toml(&self, dir: &Path) -> Result<(), Error> {
622        let contents = "\
623disable_all_formatting = true
624";
625
626        let file = dir.join("rustfmt.toml");
627
628        fs::write(file, contents).with_context(|| "error writing rustfmt.toml")?;
629
630        Ok(())
631    }
632}
633
634struct Type {
635    module_name: String,
636    type_names: Vec<String>,
637    contents: TokenStream,
638}
639
640struct ModuleTrie {
641    submodules: BTreeMap<String, ModuleTrie>,
642    types: Vec<Type>,
643}
644
645impl ModuleTrie {
646    fn new() -> ModuleTrie {
647        ModuleTrie {
648            submodules: BTreeMap::new(),
649            types: vec![],
650        }
651    }
652
653    fn insert(&mut self, module_path: &[String], type_: Type) {
654        match module_path.split_first() {
655            Some((first, rest)) => self
656                .submodules
657                .entry(first.clone())
658                .or_insert_with(ModuleTrie::new)
659                .insert(rest, type_),
660            None => self.types.push(type_),
661        }
662    }
663
664    fn render(&self, dir: &Path, lib_root: bool) -> Result<(), Error> {
665        fs::create_dir_all(dir)
666            .with_context(|| format!("error creating directory {}", dir.display()))?;
667
668        for type_ in &self.types {
669            self.write_module(
670                &dir.join(format!("{}.rs", type_.module_name)),
671                &type_.contents,
672            )?;
673        }
674
675        for (name, module) in &self.submodules {
676            module.render(&dir.join(name), false)?;
677        }
678
679        let root = self.create_root_module(lib_root);
680        let file_name = if lib_root { "lib.rs" } else { "mod.rs" };
681        self.write_module(&dir.join(file_name), &root)?;
682
683        Ok(())
684    }
685
686    fn write_module(&self, path: &Path, contents: &TokenStream) -> Result<(), Error> {
687        let file = syn::parse2(contents.clone())?;
688        let formatted = prettyplease::unparse(&file);
689
690        fs::write(path, formatted)
691            .with_context(|| format!("error writing module {}", path.display()))?;
692        Ok(())
693    }
694
695    fn create_root_module(&self, lib_root: bool) -> TokenStream {
696        let attrs = if lib_root {
697            quote! {
698                #![allow(warnings)]
699            }
700        } else {
701            quote! {}
702        };
703
704        let uses = self.types.iter().map(|m| {
705            let module_name = m.module_name.parse::<TokenStream>().unwrap();
706            let type_names = m
707                .type_names
708                .iter()
709                .map(|n| n.parse::<TokenStream>().unwrap());
710            quote! {
711                #[doc(inline)]
712                pub use self::#module_name::{#(#type_names),*};
713            }
714        });
715
716        let type_mods = self.types.iter().map(|m| {
717            let module_name = m.module_name.parse::<TokenStream>().unwrap();
718            quote! {
719                pub mod #module_name;
720            }
721        });
722
723        let sub_mods = self.submodules.keys().map(|v| {
724            let module_name = v.parse::<TokenStream>().unwrap();
725            quote! {
726                pub mod #module_name;
727            }
728        });
729
730        quote! {
731            #attrs
732            #(#uses)*
733
734            #(#type_mods)*
735            #(#sub_mods)*
736        }
737    }
738}