cynic_codegen/
registration.rs

1use std::{
2    borrow::Cow,
3    io::Write,
4    path::{Path, PathBuf},
5};
6
7use once_cell::unsync::OnceCell;
8
9use crate::schema::{self, Schema, SchemaInput};
10
11/// Registers a schema with cynic-codegen with the given name
12///
13/// This will prepare the schema for use and write intermediate files
14/// into the current crates target directory.  You can then refer to
15/// the schema by name when working with cynics macros.
16///
17/// This is designed to be called from `build.rs`
18pub fn register_schema(name: &str) -> SchemaRegistrationBuilder<'_> {
19    SchemaRegistrationBuilder {
20        name,
21        dry_run: false,
22    }
23}
24
25#[derive(thiserror::Error, Debug)]
26#[error("Could not register schema with cynic")]
27pub enum SchemaRegistrationError {
28    #[error("IOError: {0}")]
29    IoError(#[from] std::io::Error),
30    #[error("Could not find the OUT_DIR environment variable, which should be set by cargo")]
31    OutDirNotSet,
32    #[error("Errors when parsing schema: {0}")]
33    SchemaErrors(String),
34}
35
36#[must_use]
37/// An incomplete schema registration.
38///
39/// Call one of the methods on this type to provide the schema details
40pub struct SchemaRegistrationBuilder<'a> {
41    name: &'a str,
42    dry_run: bool,
43}
44
45impl<'a> SchemaRegistrationBuilder<'a> {
46    /// Pulls schema information from the SDL file at `path`
47    pub fn from_sdl_file(
48        self,
49        path: impl AsRef<std::path::Path>,
50    ) -> Result<SchemaRegistration<'a>, SchemaRegistrationError> {
51        let SchemaRegistrationBuilder { name, dry_run } = self;
52        fn inner<'a>(
53            name: &'a str,
54            path: &Path,
55            dry_run: bool,
56        ) -> Result<SchemaRegistration<'a>, SchemaRegistrationError> {
57            let data = std::fs::read_to_string(path)?;
58            let registration = SchemaRegistration {
59                name,
60                data: Cow::Owned(data),
61                schema: OnceCell::default(),
62                dry_run,
63            };
64            registration.write(registration.filename()?)?;
65            registration.write_schema_module()?;
66            cargo_rerun_if_changed(path.as_os_str().to_str().expect("utf8 paths"));
67            Ok(registration)
68        }
69
70        inner(name, path.as_ref(), dry_run)
71    }
72
73    /// Registers a schema from a string of SDL
74    pub fn from_sdl(self, sdl: &'a str) -> Result<SchemaRegistration<'a>, SchemaRegistrationError> {
75        let SchemaRegistrationBuilder { name, dry_run } = self;
76        let registration = SchemaRegistration {
77            name,
78            data: Cow::Borrowed(sdl),
79            schema: OnceCell::default(),
80            dry_run,
81        };
82        registration.write(registration.filename()?)?;
83        registration.write_schema_module()?;
84        Ok(registration)
85    }
86
87    #[doc(hidden)]
88    /// Function for benchmarks that prevents files being written
89    pub fn dry_run(mut self) -> Self {
90        self.dry_run = true;
91        self
92    }
93}
94
95/// A complete schema registration.
96///
97/// Additional methods can be called on this to
98pub struct SchemaRegistration<'a> {
99    name: &'a str,
100    data: Cow<'a, str>,
101    schema: OnceCell<Schema<'a, schema::Validated>>,
102    dry_run: bool,
103}
104
105// Public API
106impl SchemaRegistration<'_> {
107    /// Registers this schema as the default.
108    ///
109    /// The default schema (if any) will be used when you don't provide a schema
110    /// name to any of the cynic macros.
111    ///
112    /// You should only call this once per crate - any subsequent calls will overwrite
113    /// the default.
114    pub fn as_default(self) -> Result<Self, SchemaRegistrationError> {
115        if self.dry_run {
116            return Ok(self);
117        }
118        self.write(default_filename(&out_dir()?))?;
119        Ok(self)
120    }
121}
122
123// Private API
124impl SchemaRegistration<'_> {
125    fn write(&self, mut filename: PathBuf) -> Result<(), SchemaRegistrationError> {
126        if self.dry_run {
127            return Ok(());
128        }
129        std::fs::create_dir_all(filename.parent().expect("filename to have a parent"))?;
130        #[cfg(feature = "rkyv")]
131        {
132            filename.set_extension("rkyv");
133
134            let optimised = self.schema()?.optimise();
135            let bytes = rkyv::to_bytes::<_, 4096>(&optimised).unwrap();
136
137            Ok(std::fs::write(filename, &bytes)?)
138        }
139        #[cfg(not(feature = "rkyv"))]
140        {
141            filename.set_extension("graphql");
142            Ok(std::fs::write(filename, self.data.as_bytes())?)
143        }
144    }
145
146    fn write_schema_module(&self) -> Result<(), SchemaRegistrationError> {
147        use crate::use_schema::use_schema_impl;
148
149        let tokens = use_schema_impl(self.schema()?)
150            .map_err(|errors| SchemaRegistrationError::SchemaErrors(errors.to_string()))?;
151
152        if self.dry_run {
153            // This skips the token writing part which is a shame, as I'd like
154            // to benchmark that. But lets see where we get to without it
155            return Ok(());
156        }
157
158        let schema_module_filename = schema_module_filename(self.name, &out_dir()?);
159        std::fs::create_dir_all(
160            schema_module_filename
161                .parent()
162                .expect("filename to have a parent"),
163        )?;
164
165        let mut out = std::fs::File::create(&schema_module_filename)?;
166        write!(&mut out, "{}", tokens)?;
167
168        Ok(())
169    }
170
171    fn filename(&self) -> Result<PathBuf, SchemaRegistrationError> {
172        if self.dry_run {
173            return Ok(PathBuf::from(""));
174        }
175        let out_dir = out_dir()?;
176        Ok(registration_filename(self.name, &out_dir))
177    }
178
179    fn schema(&self) -> Result<&Schema<'_, schema::Validated>, SchemaRegistrationError> {
180        self.schema.get_or_try_init(|| {
181            let ast = crate::schema::load_schema(self.data.as_ref())
182                .map_err(|error| SchemaRegistrationError::SchemaErrors(error.to_string()))?;
183
184            let schema = Schema::new(SchemaInput::Document(ast))
185                .validate()
186                .map_err(|errors| SchemaRegistrationError::SchemaErrors(errors.to_string()))?;
187
188            Ok(schema)
189        })
190    }
191}
192
193fn cargo_rerun_if_changed(name: &str) {
194    println!("cargo:rerun-if-changed={name}");
195}
196
197pub(super) fn out_dir() -> Result<String, SchemaRegistrationError> {
198    let out_dir = std::env::var("OUT_DIR").map_err(|_| SchemaRegistrationError::OutDirNotSet)?;
199    Ok(out_dir)
200}
201
202pub(super) fn schema_module_filename(name: &str, out_dir: &str) -> PathBuf {
203    let mut path = PathBuf::from(out_dir);
204    path.push("cynic-schemas");
205    path.push(format!("{name}.rs",));
206
207    path
208}
209
210fn registration_filename(name: &str, out_dir: &str) -> PathBuf {
211    let mut path = PathBuf::from(out_dir);
212    path.push("cynic-schemas");
213    path.push(format!("{name}.graphql",));
214
215    path
216}
217
218fn default_filename(out_dir: &str) -> PathBuf {
219    let mut path = PathBuf::from(out_dir);
220    path.push("cynic-schemas");
221    path.push("default.graphql");
222
223    path
224}