diff --git a/crates/generator/Cargo.lock b/crates/generator/Cargo.lock new file mode 100644 index 0000000..daf21d8 --- /dev/null +++ b/crates/generator/Cargo.lock @@ -0,0 +1,290 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "generator" +version = "0.1.0" +dependencies = [ + "fs-err", + "handlebars", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/crates/generator/Cargo.toml b/crates/generator/Cargo.toml new file mode 100644 index 0000000..cbc68e1 --- /dev/null +++ b/crates/generator/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] + +[package] +name = "generator" +publish = false +version = "0.1.0" +edition = "2021" + +[dependencies] +fs-err = "2.11.0" +handlebars = "5.1.2" diff --git a/crates/generator/src/main.rs b/crates/generator/src/main.rs new file mode 100644 index 0000000..6f0c3ca --- /dev/null +++ b/crates/generator/src/main.rs @@ -0,0 +1,41 @@ +use std::{collections::BTreeMap, env, path::Path}; + +use handlebars::Handlebars; + +fn main() { + let versions: Vec = env::args() + .skip(1) // skip the binary's own name, only keep explicit args + .map(|value| { + value + .parse() + .unwrap_or_else(|e| panic!("invalid version \"{value}\": {e}")) + }) + .collect(); + let mut handlebars = Handlebars::new(); + let mut args = BTreeMap::new(); + args.insert("version_numbers", versions.as_slice()); + + let template_and_output = [ + ("../../template/parser.template.rs", "../../src/parser.rs"), + ("../../template/query.template.rs", "../../src/query.rs"), + ( + "../../template/versioned.template.rs", + "../../src/versioned.rs", + ), + ]; + for (template_file, target) in template_and_output { + let output = materialize(&mut handlebars, Path::new(template_file), &args); + fs_err::write(Path::new(target), output).expect("failed to write file"); + } +} + +fn materialize( + handlebars: &mut Handlebars, + template_file: &Path, + args: &BTreeMap<&str, &[i64]>, +) -> String { + let template = fs_err::read_to_string(template_file).expect("failed to read file"); + handlebars + .render_template(&template, args) + .expect("failed to render template") +} diff --git a/scripts/add_next_rustdoc_version.sh b/scripts/add_next_rustdoc_version.sh new file mode 100644 index 0000000..8b943d5 --- /dev/null +++ b/scripts/add_next_rustdoc_version.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Script requirements: +# - python +# - yq + +# Fail on first error, on undefined variables, and on failures in pipelines. +set -euo pipefail + +# Go to the repo root directory. +cd "$(git rev-parse --show-toplevel)" + +CURRENT_VERSIONS="$(yq '.features.default' Cargo.toml -o json | \ + python -m json.tool --compact | \ + sed 's/,/, /g' | \ + sed 's/\[//g' | \ + sed 's/]//g')" +NEXT_VERSION_NUMBER="$(yq '.features.default.[-1] | sub("v(\d+)", "${1}") | to_number | (. + 1)' Cargo.toml -o json -r)" + +ALL_VERSIONS="$(yq '.features.default[] | sub("v(\d+)", "${1}")' Cargo.toml -o json -r) ${NEXT_VERSION_NUMBER}" + +# Generate the new Rust source for the specified versions. +pushd crates/generator +cargo run -- $ALL_VERSIONS +popd + +# Reformat the generated Rust source code. +cargo fmt + +# Update the Cargo.toml file to add the new dependency and feature number. + +# '1h;2,$H;$!d;g' means "look two lines at a time": +# https://unix.stackexchange.com/questions/26284/how-can-i-use-sed-to-replace-a-multi-line-string +sed -e '1h;2,$H;$!d;g' \ + -e "s/\n\[features\]/trustfall-rustdoc-adapter-v${NEXT_VERSION_NUMBER} = { package = \"trustfall-rustdoc-adapter\", version = \">=${NEXT_VERSION_NUMBER}.0.0,<${NEXT_VERSION_NUMBER}.1.0\", optional = true }\n\n[features]/" \ + -i Cargo.toml + +DEFAULT_MATCHER="default = \[${CURRENT_VERSIONS}\]" +sed -e "s/$DEFAULT_MATCHER/default = [${CURRENT_VERSIONS}, \"v${NEXT_VERSION_NUMBER}\"]/" \ + -i Cargo.toml + +echo "v${NEXT_VERSION_NUMBER} = [\"dep:trustfall-rustdoc-adapter-v${NEXT_VERSION_NUMBER}\"]" >>Cargo.toml + +# Ensure cargo regenerates the lockfile. +cargo check diff --git a/template/parser.template.rs b/template/parser.template.rs new file mode 100644 index 0000000..ea1cf17 --- /dev/null +++ b/template/parser.template.rs @@ -0,0 +1,60 @@ +use std::path::Path; +use std::{fs::File, io::Read}; + +use anyhow::{bail, Context}; +use serde::Deserialize; + +use crate::versioned::VersionedCrate; + +#[derive(Deserialize)] +struct RustdocFormatVersion { + format_version: u32, +} + +fn detect_rustdoc_format_version(path: &Path, file_data: &str) -> anyhow::Result { + let version = serde_json::from_str::(file_data) + .with_context(|| format!("unrecognized rustdoc format for file {}", path.display()))?; + + Ok(version.format_version) +} + +fn parse_or_report_error(path: &Path, file_data: &str, format_version: u32) -> anyhow::Result +where + T: for<'a> Deserialize<'a>, +{ + serde_json::from_str(file_data).with_context(|| { + format!( + "unexpected parse error for v{format_version} rustdoc for file {}", + path.display() + ) + }) +} + +pub fn load_rustdoc(path: &Path) -> anyhow::Result { + // Parsing JSON after fully reading a file into memory is much faster than + // parsing directly from a file, even if buffered: + // https://github.com/serde-rs/json/issues/160 + let mut file_data = String::new(); + File::open(path) + .with_context(|| format!("failed to open rustdoc JSON file {}", path.display()))? + .read_to_string(&mut file_data) + .with_context(|| format!("failed to read rustdoc JSON file {}", path.display()))?; + + let format_version = detect_rustdoc_format_version(path, &file_data)?; + + match format_version { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + {{this}} => Ok(VersionedCrate::V{{this}}(parse_or_report_error( + path, + &file_data, + format_version, + )?)), + + {{/each}} + _ => bail!( + "rustdoc format v{format_version} for file {} is not supported", + path.display() + ), + } +} diff --git a/template/query.template.rs b/template/query.template.rs new file mode 100644 index 0000000..69ba71e --- /dev/null +++ b/template/query.template.rs @@ -0,0 +1,25 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use trustfall::{execute_query, FieldValue}; + +use crate::versioned::VersionedRustdocAdapter; + +type QueryResult = BTreeMap, FieldValue>; + +impl<'a> VersionedRustdocAdapter<'a> { + pub fn run_query>, V: Into>( + &self, + query: &str, + vars: BTreeMap, + ) -> anyhow::Result + 'a>> { + match self { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + VersionedRustdocAdapter::V{{this}}(_, adapter) => { + execute_query(self.schema(), adapter.clone(), query, vars) + } + + {{/each}} + } + } +} diff --git a/template/versioned.template.rs b/template/versioned.template.rs new file mode 100644 index 0000000..1b3151e --- /dev/null +++ b/template/versioned.template.rs @@ -0,0 +1,132 @@ +use std::{fmt::Debug, sync::Arc}; + +use anyhow::bail; +use trustfall::Schema; + +macro_rules! add_version_method { + () => { + pub fn version(&self) -> u32 { + match self { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + Self::V{{this}}(..) => {{this}}, + + {{/each}} + } + } + }; +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum VersionedCrate { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + V{{this}}(trustfall_rustdoc_adapter_v{{this}}::Crate), + + {{/each}} +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum VersionedIndexedCrate<'a> { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + V{{this}}(trustfall_rustdoc_adapter_v{{this}}::IndexedCrate<'a>), + + {{/each}} +} + +#[non_exhaustive] +pub enum VersionedRustdocAdapter<'a> { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + V{{this}}( + Schema, + Arc>, + ), + + {{/each}} +} + +impl VersionedCrate { + pub fn crate_version(&self) -> Option<&str> { + match self { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + VersionedCrate::V{{this}}(c) => c.crate_version.as_deref(), + + {{/each}} + } + } + + add_version_method!(); +} + +impl<'a> VersionedIndexedCrate<'a> { + pub fn new(crate_: &'a VersionedCrate) -> Self { + match &crate_ { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + VersionedCrate::V{{this}}(c) => { + Self::V{{this}}(trustfall_rustdoc_adapter_v{{this}}::IndexedCrate::new(c)) + } + + {{/each}} + } + } + + add_version_method!(); +} + +impl<'a> VersionedRustdocAdapter<'a> { + pub fn new( + current: &'a VersionedIndexedCrate, + baseline: Option<&'a VersionedIndexedCrate>, + ) -> anyhow::Result { + match (current, baseline) { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + (VersionedIndexedCrate::V{{this}}(c), Some(VersionedIndexedCrate::V{{this}}(b))) => { + let adapter = Arc::new(trustfall_rustdoc_adapter_v{{this}}::RustdocAdapter::new( + c, + Some(b), + )); + Ok(VersionedRustdocAdapter::V{{this}}( + trustfall_rustdoc_adapter_v{{this}}::RustdocAdapter::schema(), + adapter, + )) + } + + #[cfg(feature = "v{{this}}")] + (VersionedIndexedCrate::V{{this}}(c), None) => { + let adapter = Arc::new(trustfall_rustdoc_adapter_v{{this}}::RustdocAdapter::new(c, None)); + Ok(VersionedRustdocAdapter::V{{this}}( + trustfall_rustdoc_adapter_v{{this}}::RustdocAdapter::schema(), + adapter, + )) + } + + {{/each}} + (c, Some(b)) => { + bail!( + "version mismatch between current (v{}) and baseline (v{}) format versions", + c.version(), + b.version() + ) + } + } + } + + pub fn schema(&self) -> &Schema { + match self { + {{#each version_numbers}} + #[cfg(feature = "v{{this}}")] + VersionedRustdocAdapter::V{{this}}(schema, ..) => schema, + + {{/each}} + } + } + + add_version_method!(); +}