From 7f8103101a309cae9517d53b86481da67ba1958e Mon Sep 17 00:00:00 2001 From: Cerber Ursi Date: Sat, 27 Nov 2021 19:10:28 +0700 Subject: [PATCH] Documentation of "how it works"; rustfmt --- src/error.rs | 7 +- src/funcs.rs | 15 +-- src/helpers.rs | 7 +- src/lib.rs | 205 ++++++++++++++++++++++++++++++++++++-- test_fixtures/data.toml | 2 + tests/generate_and_use.rs | 25 +++-- 6 files changed, 238 insertions(+), 23 deletions(-) diff --git a/src/error.rs b/src/error.rs index 755fccf..ef0be93 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,7 +14,10 @@ pub enum UnevalError { } impl ser::Error for UnevalError { - fn custom(msg:T)->Self where T: std::fmt::Display { + fn custom(msg: T) -> Self + where + T: std::fmt::Display, + { Self::Custom(msg.to_string()) } -} \ No newline at end of file +} diff --git a/src/funcs.rs b/src/funcs.rs index de3fa91..01a6ba0 100644 --- a/src/funcs.rs +++ b/src/funcs.rs @@ -1,7 +1,7 @@ //! Convenience functions to be used with Uneval. -use crate::ser::{SerResult, Uneval}; use crate::error::UnevalError; +use crate::ser::{SerResult, Uneval}; use serde::Serialize; /// Write generated Rust code to the provided [`Write`][std::io::Write] implementation. @@ -35,14 +35,17 @@ pub fn to_file(value: impl Serialize, target: impl AsRef) -> Se } /// Convenience wrapper around [`to_file`]. -/// +/// /// This function finds out where the output directory is by looking at `OUT_DIR` environment variable /// and creates the file with the provided name there. pub fn to_out_dir(value: impl Serialize, file_name: impl AsRef) -> SerResult { let path: std::path::PathBuf = [ - std::env::var("OUT_DIR").expect("OUT_DIR not set, check if you're running this from the build script"), - file_name.as_ref().into() - ].iter().collect(); + std::env::var("OUT_DIR") + .expect("OUT_DIR not set, check if you're running this from the build script"), + file_name.as_ref().into(), + ] + .iter() + .collect(); value.serialize(&mut Uneval::new(std::fs::File::create(path)?)) } @@ -51,4 +54,4 @@ pub fn to_string(value: impl Serialize) -> Result { let mut out = Vec::new(); value.serialize(&mut Uneval::new(&mut out))?; Ok(String::from_utf8(out)?) -} \ No newline at end of file +} diff --git a/src/helpers.rs b/src/helpers.rs index 8b5ddaa..10a1285 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -2,11 +2,14 @@ use crate::ser::SerResult; use std::io::Write; pub(crate) fn tuple_converter(mut output: impl Write, len: usize) -> SerResult { - write!(output, " + write!( + output, + " trait FromTuple: Sized {{ fn from_tuple(tuple: T) -> Self; }} - ")?; + " + )?; let array = format!("[T; {}]", len); let tuple = format!("({})", (0..len).map(|_| "T,").collect::()); let mapping = format!( diff --git a/src/lib.rs b/src/lib.rs index 6988fc9..629b92f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,8 @@ //! data, like JSON or YAML. This way, you'll mostly escape runtime cost of deserialization, //! nearly as if you've written the same data directly in code by hand. //! Of course, in most cases this cost is already negligible, but for crates which use -//! large blobs of data this crate can come in handy, improving startup times. +//! large blobs of data this crate can come in handy, improving startup times, and can +//! eliminate the need for `serde` as runtime dependency. //! //! ## Usage //! In general, to embed some code into crate, you have to use the build script @@ -15,6 +16,197 @@ //! let value = include!(concat!(env!(OUT_DIR), "/file_name.rs")); //! ``` //! +//! ## How does it work? +//! +//! Of course, we can't always directly construct the code for the desired value (more on this +//! in the [Limitations](#limitations) section below). +//! However, in many cases the information provided by Serde is enough. +//! +//! For every case, we'll provide an example of how the generated code can look like, as a sequence of +//! `let` statements, where the left part is written by hand and the right one is assumed to be generated. +//! +//! ### Primitives +//! +//! Number literals, such as `i8` or `f32`, are directly written into the output. The only tricky part +//! is that we have to use suffixed literals, e.g. `1u8` or `1.1f64` - otherwise we'd run into the problem +//! with the float values which are in fact integers, since they would be output as integer literals, +//! not as float ones (i.e. `1` and not `1.0`) and so wouldn't typecheck. +//! +//! Boolean and character literals are also simply written directly - no surprises here. +//! +//! Example: +//! ``` +//! let _: i8 = 12i8; +//! let _: u128 = 12345u128; +//! let _: f32 = -1f32; +//! let _: f64 = 12345.6789f64; +//! let _: char = 'c'; +//! let _: bool = true; +//! ``` +//! +//! ### Strings +//! When Serde gives us something string-like, we have to make some kind of conversion, since +//! string literals are of type `&'static str`, and string-like fields in serializable structs are +//! usually of some owned type, like `String`. We assume that every such type would be convertible to +//! `String` using [`Into`][std::convert::Into], so we simply emit a string literal with call to `into`. +//! +//! Example: +//! ``` +//! let _: String = "string value".into(); +//! ``` +//! +//! Byte strings are handled as byte sequences, [as recommended by Serde itself][::serde::Serializer::serialize_bytes], +//! and so we'll discuss them [below](#vec-like-types-sequences). +//! +//! ### Tuple structs and unit values +//! +//! Unit type (`()`), unit structs and unit variants (including `None`) are emitted simply by using +//! the type name. Tuple structs and variants (and newtype-flavored ones, including `Some`) +//! are emitted by writing their name (with the enum name, if necessary), parenthesis, +//! and serializing the inner values. +//! +//! Example: +//! ``` +//! struct TupleStruct((), Option, Option); +//! let _: TupleStruct = TupleStruct((), None, Some(1u8)); +//! ``` +//! +//! ### Vec-like types (sequences) +//! +//! `Vec`-like structures are constructed using the temporary `Vec`. We assume that every such type will +//! implement [`FromIterator`][std::iter::FromIterator], so we emit the call to `vec!` macro, +//! serialize the data and finalize the emit with call to `into_iter().collect()`. +//! This is not exactly zero-cost, but it seems that this is the minimal. +//! +//! Example: +//! ``` +//! let _: Vec = vec![1u32, 2u32, 3u32].into_iter().collect(); +//! ``` +//! +//! ### Tuples and arrays +//! +//! That's where it becomes tricky. +//! +//! The problem is that Serde doesn't distinguish between this two kinds of values: they both are treated +//! as sequences with known length, called "tuples" internally; as a consequence, we don't know at the emit time, +//! which of them we'll be generating. But in the Rust code, they are created with entirely different syntax, +//! and there's no easy way to convert one into another. So, we decided to emit a little "runtime" +//! (consisting of small `#[inline]` functions, so it should in fact be zero-cost), which will +//! correctly handle the data according to the type being requested. +//! +//! The idea is, in fact, directly borrowed from the [`collect`]/[`FromIterator`] pair: we can call `collect` +//! on every iterator value, and, as long as the target type implements `FromIterator` with the necessary +//! parameters, `collect` will do its job. We're using not the trait method, but the free function (the reason is +//! that with the trait we would sometimes have a chain of type inferences, which Rust is unable to solve); +//! however, this doesn't change the overall picture. +//! +//! [`collect`]: std::iter::Iterator::collect +//! [`FromIterator`]: std::iter::FromIterator +//! +//! In general, here's what being generated: +//! - A `FromTuple` trait with `from_tuple(input: T) -> Self` associated function. +//! - Two implementations: `impl FromTuple<(T,...,T,)> for [T; N]` and +//! `impl FromTuple<(T1,...TN,)> for (T1,...TN,)`. +//! - Function `convert>(tuple: (T1,...TN,)) -> Out`, +//! which simply calls `Out::from_tuple(tuple)`. +//! +//! Then, the value itself is created by the call to `convert`, with tuple of serialized values as argument. +//! Depending on whether the target expects the array or tuple, `convert` will select one particular implementation. +//! +//! Example: +//! ``` +//! let tuple: (i32, f32, String) = { +//! trait FromTuple: Sized { +//! fn from_tuple(tuple: T) -> Self; +//! } +//! +//! impl FromTuple<(T,T,T,)> for [T; 3] { +//! #[inline] +//! fn from_tuple(tuple: (T,T,T,)) -> Self { +//! [tuple.0,tuple.1,tuple.2] +//! } +//! } +//! +//! impl FromTuple<(T0,T1,T2,)> for (T0,T1,T2,) { +//! #[inline] +//! fn from_tuple(tuple: (T0,T1,T2,)) -> Self { +//! tuple +//! } +//! } +//! +//! #[inline] +//! fn convert>(tuple: (T0,T1,T2,)) -> Out { +//! Out::from_tuple(tuple) +//! } +//! +//! convert((1i32,1f32,"tuple entry".into())) +//! }; +//! // Check that the tuple is indeed created as desired. +//! assert_eq!(tuple, (1i32,1f32,"tuple entry".to_string())); +//! +//! let arr: [i32; 4] = { +//! trait FromTuple: Sized { +//! fn from_tuple(tuple: T) -> Self; +//! } +//! +//! impl FromTuple<(T,T,T,T,)> for [T; 4] { +//! #[inline] +//! fn from_tuple(tuple: (T,T,T,T,)) -> Self { +//! [tuple.0,tuple.1,tuple.2,tuple.3] +//! } +//! } +//! +//! impl FromTuple<(T0,T1,T2,T3,)> for (T0,T1,T2,T3,) { +//! #[inline] +//! fn from_tuple(tuple: (T0,T1,T2,T3,)) -> Self { +//! tuple +//! } +//! } +//! +//! #[inline] +//! fn convert>(tuple: (T0,T1,T2,T3,)) -> Out { +//! Out::from_tuple(tuple) +//! } +//! +//! convert((1,2,3,4)) +//! }; +//! // Check that the array is indeed created as desired. +//! assert_eq!(arr, [1, 2, 3, 4]); +//! ``` +//! +//! ### Maps +//! +//! Since Rust doesn't have the notion of map literals, we can't construct one directly. However, standard map-like +//! types ([`HashMap`], [`BTreeMap`]) implement `FromIterator<(K, V)>`, i.e. they can be built from the iterator of +//! key-value pairs. `uneval` generates code according to this convention: we create a `Vec` of pairs, which is then +//! converted into map with `into_iter().collect()`. +//! +//! Example: +//! ``` +//! let _: std::collections::HashMap = vec![ +//! (1, "first".into()), +//! (100, "one hundredth".into()), +//! ].into_iter().collect(); +//! ``` +//! +//! [`HashMap`]: std::collections::HashMap +//! [`BTreeMap`]: std::collections::BTreeMap +//! +//! ### Structs +//! +//! Last but not the least, this case is relatively simple. Emitted code is simply the struct construction - +//! i.e. the struct name, the curly braces and a list of pairs of the form `{field name}: {serialized value}`. +//! +//! Example: +//! ``` +//! struct Struct { boolean: bool, number: i32, string: String } +//! let _: Struct = Struct { +//! boolean: true, +//! number: 1i32, +//! string: "string".into() +//! }; +//! ``` +//! //! ## Limitations //! There are some cases when `uneval` will be unable to generate valid code. Namely: //! 1. Since Serde doesn't provide us the full path to the type in question (and in most cases it's simply unable to), @@ -33,9 +225,10 @@ //! //! [include]: https://doc.rust-lang.org/stable/std/macro.include.html -pub mod error; -pub mod ser; -pub mod funcs; -pub mod helpers; +mod helpers; -pub use funcs::{to_file, to_out_dir, to_string, write}; \ No newline at end of file +pub mod error; +pub mod funcs; +pub mod ser; + +pub use funcs::{to_file, to_out_dir, to_string, write}; diff --git a/test_fixtures/data.toml b/test_fixtures/data.toml index 7dfdcb9..c6a3652 100644 --- a/test_fixtures/data.toml +++ b/test_fixtures/data.toml @@ -49,6 +49,7 @@ use std::collections::HashMap; pub struct Complex { pub tuple: (i32, f32, String), pub vector: Vec, + pub byte_arr: Vec, pub map: HashMap, } """ @@ -60,6 +61,7 @@ value = """ definition::Complex { tuple: (1, 1.0, "tuple entry".into()), vector: vec![1, 2, 3, 4, 5, -1, -2, -3, -4, -5], + byte_arr: b"asdfghj".to_vec(), map } } diff --git a/tests/generate_and_use.rs b/tests/generate_and_use.rs index 10ff2ed..3555702 100644 --- a/tests/generate_and_use.rs +++ b/tests/generate_and_use.rs @@ -1,10 +1,11 @@ use batch_run::Batch; +use serde::Deserialize; use std::{ + collections::HashMap, fs::{create_dir, read_to_string, File}, io::Write, - path::{Path, PathBuf}, collections::HashMap, + path::{Path, PathBuf}, }; -use serde::Deserialize; use toml::from_str; #[derive(Deserialize, Default)] @@ -39,19 +40,28 @@ impl Data { write!( File::create(&path.with_file_name(format!("{}-user.rs", name))).unwrap(), include_str!("user.tpl"), - types = self.support_types.as_ref().map_or(self.main_type.clone(), |types| format!("{},{}", self.main_type, types)), + types = self + .support_types + .as_ref() + .map_or(self.main_type.clone(), |types| format!( + "{},{}", + self.main_type, types + )), ser_type = self.main_type, value = self.value - ).unwrap(); + ) + .unwrap(); write!( File::create(&path.with_file_name(format!("{}-main.snapshot", name))).unwrap(), include_str!("main.snapshot.tpl"), name = name - ).unwrap(); + ) + .unwrap(); write!( File::create(&path.with_file_name(format!("{}-user.snapshot", name))).unwrap(), include_str!("user.snapshot.tpl"), - ).unwrap(); + ) + .unwrap(); } } @@ -60,7 +70,8 @@ fn main() { let toml = read_to_string("test_fixtures/data.toml").unwrap(); let data: HashMap = from_str(&toml).unwrap(); let path: PathBuf = "test_fixtures".into(); - data.into_iter().for_each(|(key, value)| value.write(&key, &path)); + data.into_iter() + .for_each(|(key, value)| value.write(&key, &path)); let b = Batch::new(); b.run_match("test_fixtures/**/*-main.rs");