feat: add Fuji Modelling Language codegen
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -17,19 +17,26 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Lint
|
||||
run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all-features --all-targets
|
||||
- name: Install CUE
|
||||
run: |
|
||||
CUE_VERSION=$(curl -fsSL https://api.github.com/repos/cue-lang/cue/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
|
||||
curl -fsSL "https://github.com/cue-lang/cue/releases/download/${CUE_VERSION}/cue_${CUE_VERSION}_linux_amd64.tar.gz" | tar -xzC /tmp
|
||||
sudo mv /tmp/cue /usr/local/bin/cue
|
||||
|
||||
- name: Compile
|
||||
run: cargo check --all-features --all-targets
|
||||
run: cargo check --all-features --all-targets --workspace
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --all --check
|
||||
|
||||
- name: Lint
|
||||
run: cargo clippy --all-features --all-targets --workspace -- -D warnings
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all-features --all-targets --workspace
|
||||
|
||||
release:
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
@@ -43,6 +50,14 @@ jobs:
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install CUE
|
||||
run: |
|
||||
CUE_VERSION=$(curl -fsSL https://api.github.com/repos/cue-lang/cue/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
|
||||
curl -fsSL "https://github.com/cue-lang/cue/releases/download/${CUE_VERSION}/cue_${CUE_VERSION}_linux_amd64.tar.gz" | tar -xzC /tmp
|
||||
sudo mv /tmp/cue /usr/local/bin/cue
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --all-features --all-targets
|
||||
|
||||
@@ -26,3 +26,10 @@ result-*
|
||||
.direnv
|
||||
|
||||
.cargo/
|
||||
|
||||
# ---> FML
|
||||
# Artifacts from `cue export ./fml`
|
||||
*.json
|
||||
|
||||
# Build-time codegen output. Inspect locally; never commit.
|
||||
src/lib/generated/
|
||||
|
||||
Generated
+27
-20
@@ -175,6 +175,20 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "codegen"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -338,23 +352,22 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "fujicli"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"clap",
|
||||
"codegen",
|
||||
"erased-serde",
|
||||
"exiftool",
|
||||
"log",
|
||||
"log4rs",
|
||||
"num_enum",
|
||||
"paste",
|
||||
"ptp_cursor",
|
||||
"ptp_macro",
|
||||
"rusb",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serde_with",
|
||||
"strsim",
|
||||
"strum",
|
||||
@@ -649,12 +662,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -676,6 +683,16 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
@@ -707,6 +724,7 @@ name = "ptp_macro"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num_enum",
|
||||
"proc-macro2",
|
||||
"ptp_cursor",
|
||||
"quote",
|
||||
@@ -930,17 +948,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.16.1"
|
||||
|
||||
+14
-3
@@ -1,11 +1,20 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"crates/codegen",
|
||||
"crates/ptp/cursor",
|
||||
"crates/ptp/macro",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "fujicli"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
description = "A CLI to manage Fujifilm devices, simulations, backups, and rendering"
|
||||
authors = [
|
||||
"Nikolaos Karaolidis <nick@karaolidis.com>",
|
||||
]
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "fujicli"
|
||||
@@ -15,6 +24,10 @@ path = "src/lib/mod.rs"
|
||||
name = "fujicli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.100"
|
||||
codegen = { path = "crates/codegen" }
|
||||
|
||||
[profile.release]
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
@@ -36,9 +49,7 @@ ptp_macro = { path = "crates/ptp/macro" }
|
||||
ptp_cursor = { path = "crates/ptp/cursor" }
|
||||
strum = { version = "0.27.2", features = ["strum_macros"] }
|
||||
strum_macros = "0.27.2"
|
||||
paste = "1.0.15"
|
||||
erased-serde = "0.4.8"
|
||||
serde_repr = "0.1.20"
|
||||
serde_with = "3.15.1"
|
||||
exiftool = "0.3.0"
|
||||
tempfile = "3.24.0"
|
||||
|
||||
@@ -23,106 +23,40 @@ Options:
|
||||
|
||||
## Status
|
||||
|
||||
This tool has only been extensively tested with the **Fujifilm X-T5**, as it is the sole camera I own. While the underlying PTP commands may be compatible with other models, **compatibility is not guaranteed**.
|
||||
Extensively tested only with the **Fujifilm X-T5**. The underlying PTP commands
|
||||
likely work on other Fujifilm models, but **compatibility is not guaranteed**.
|
||||
|
||||
**Use this software at your own risk.** I am not responsible for any damage, loss of data, or other adverse outcomes -- physical or psychological -- to your camera or equipment resulting from the use of this program.
|
||||
**Use this software at your own risk.** The author is not responsible for any
|
||||
damage, lost data, or other adverse outcomes - physical or psychological - to
|
||||
your camera or equipment.
|
||||
|
||||
This project is currently under heavy development. Contributions are welcome. If you own a different Fujifilm camera, testing and reporting compatibility is highly appreciated.
|
||||
This project is under heavy development. Contributions are welcome. See
|
||||
[docs/users/support.md](docs/users/support.md) for the camera support matrix.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full wiki lives in [`docs/`](docs/README.md).
|
||||
|
||||
## GitHub Mirror
|
||||
|
||||
The canonical source for `fujicli` lives on my [self-hosted Gitea instance](https://git.karaolidis.com/karaolidis/fujicli). A [GitHub mirror](https://github.com/karaolidis/fujicli) exists purely for visibility and community collaboration. In practice, this means:
|
||||
The canonical source for `fujicli` lives on a
|
||||
[self-hosted Gitea instance](https://git.karaolidis.com/karaolidis/fujicli). A
|
||||
[GitHub mirror](https://github.com/karaolidis/fujicli) exists for visibility and
|
||||
community collaboration:
|
||||
|
||||
- Stars, issues, and pull requests on GitHub are welcome
|
||||
- Changes may be reviewed and merged on the primary Gitea repo first
|
||||
- GitHub may lag slightly behind the canonical repo during heavy development
|
||||
- Stars, issues, and pull requests on GitHub are welcome.
|
||||
- Changes may be reviewed and merged on the primary Gitea repo first.
|
||||
- GitHub may lag slightly behind the canonical repo during heavy development.
|
||||
|
||||
If you're contributing, testing new camera models, or reporting bugs, GitHub is totally fine. If you're looking for the absolute latest commits, the self-hosted repo is the source of truth.
|
||||
|
||||
## Requirements
|
||||
|
||||
If you are on NixOS, simply use the flake package. Otherwise:
|
||||
|
||||
- `exiftool` is required to extract simulation data from files.
|
||||
|
||||
### Windows
|
||||
|
||||
To allow `fujicli` to talk to the camera, install the `WinUSB` driver for your Fujifilm device using Zadig.
|
||||
|
||||
- Install Zadig: https://zadig.akeo.ie/
|
||||
- Connect your camera via USB in PTP/USB mode.
|
||||
- Open Zadig -> Options -> "List All Devices".
|
||||
- Select your camera (it may appear as "USB PTP" or with the model name).
|
||||
- Choose `WinUSB` (recommended) or `libusbK` as the target driver.
|
||||
- Click "Replace Driver". This disables Windows' default WPD/photo import. You can revert later from Zadig.
|
||||
|
||||
### MacOS
|
||||
|
||||
Usually no driver changes are required.
|
||||
|
||||
### Linux
|
||||
|
||||
It just works, because Linux is simply better ;).
|
||||
|
||||
If you do hit permission issues, add a udev rule for Fujifilm devices (vendor ID `0x04cb`).
|
||||
|
||||
## Camera Support
|
||||
|
||||
The following cameras are currently recognized. Feature support varies per model/generation:
|
||||
|
||||
| Model | Generation | Base Info | Backups | Simulations | Rendering |
|
||||
| --------------- | ----------- | --------- | ------- | ----------- | --------- |
|
||||
| FUJIFILM X-E1 | X-Trans | ? | | | |
|
||||
| FUJIFILM X-M1 | X-Trans | ? | | | |
|
||||
| FUJIFILM X70 | X-Trans II | ? | | | |
|
||||
| FUJIFILM X-E2 | X-Trans II | ? | | | |
|
||||
| FUJIFILM X-T1 | X-Trans II | ? | | | |
|
||||
| FUJIFILM X-T10 | X-Trans II | ? | | | |
|
||||
| FUJIFILM X100F | X-Trans III | ? | | | |
|
||||
| FUJIFILM X-E3 | X-Trans III | ? | | | |
|
||||
| FUJIFILM X-H1 | X-Trans III | ? | | | |
|
||||
| FUJIFILM X-Pro2 | X-Trans III | ? | | | |
|
||||
| FUJIFILM X-T2 | X-Trans III | ? | | | |
|
||||
| FUJIFILM X-T20 | X-Trans III | ? | | | |
|
||||
| FUJIFILM X100V | X-Trans IV | ? | | | |
|
||||
| FUJIFILM X-E4 | X-Trans IV | ? | | | |
|
||||
| FUJIFILM X-Pro3 | X-Trans IV | ? | | | |
|
||||
| FUJIFILM X-S10 | X-Trans IV | ? | | | |
|
||||
| FUJIFILM X-T3 | X-Trans IV | ? | | | |
|
||||
| FUJIFILM X-T4 | X-Trans IV | ? | | | |
|
||||
| FUJIFILM X-S20 | X-Trans IV | ✓ | ✓ | ✓ | |
|
||||
| FUJIFILM X100VI | X-Trans V | ? | | | |
|
||||
| FUJIFILM X-H2 | X-Trans V | ? | | | |
|
||||
| FUJIFILM X-H2S | X-Trans V | ? | | | |
|
||||
| FUJIFILM X-T5 | X-Trans V | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
### Legend
|
||||
|
||||
- **✓**: Known to work
|
||||
- **?**: Untested but likely works
|
||||
- **✗**: Known not to work
|
||||
- Blank: Unimplemented until further information is available
|
||||
|
||||
## Help Add Your Camera
|
||||
|
||||
If your camera isn't listed, or a feature is missing, you can help expedite support. See [support/REVERSING.md](support/REVERSING.md) for detailed instructions.
|
||||
|
||||
### Emulation Mode (`--emulate`)
|
||||
|
||||
The `--emulate` flag forces `fujicli` to treat the connected camera as a different Fujifilm model by overriding its USB vendor/product ID. This is primarily intended for development, reverse-engineering, and compatibility testing.
|
||||
|
||||
- Emulation does not magically add support for unsupported cameras
|
||||
- It may expose incorrect or unsupported PTP properties
|
||||
- Some commands (especially rendering) can and will behave unpredictably
|
||||
|
||||
If you're not actively debugging or contributing, you probably don't want this flag.
|
||||
If you're looking for the absolute latest commits, the self-hosted repo is the
|
||||
source of truth.
|
||||
|
||||
## Resources
|
||||
|
||||
This project builds upon the following fantastic reverse-engineering efforts:
|
||||
This project builds upon the following reverse-engineering efforts:
|
||||
|
||||
* [fujihack](https://github.com/fujihack/fujihack)
|
||||
* [fudge](https://github.com/petabyt/fudge)
|
||||
* [libpict](https://github.com/petabyt/libpict)
|
||||
* [fp](https://github.com/petabyt/fp)
|
||||
* [libgphoto2](https://github.com/gphoto/libgphoto2)
|
||||
- [fujihack](https://github.com/fujihack/fujihack)
|
||||
- [fudge](https://github.com/petabyt/fudge)
|
||||
- [libpict](https://github.com/petabyt/libpict)
|
||||
- [fp](https://github.com/petabyt/fp)
|
||||
- [libgphoto2](https://github.com/gphoto/libgphoto2)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use std::{path::PathBuf, process::Command, str};
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("cargo:rerun-if-changed=fml/");
|
||||
println!("cargo:rerun-if-changed=crates/codegen/");
|
||||
|
||||
let manifest = PathBuf::from(
|
||||
std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo"),
|
||||
);
|
||||
let generated = manifest.join("src").join("lib").join("generated");
|
||||
|
||||
let cue = Command::new("cue")
|
||||
.args(["export", "./fml", "--out", "json"])
|
||||
.current_dir(&manifest)
|
||||
.output()
|
||||
.map_err(|err| {
|
||||
anyhow::anyhow!("failed to invoke `cue export`: {err}. Install CUE (https://cuelang.org) or run inside `nix develop`.")
|
||||
})?;
|
||||
|
||||
if !cue.status.success() {
|
||||
bail!(
|
||||
"`cue export ./fml --out json` failed:\n{}",
|
||||
String::from_utf8_lossy(&cue.stderr),
|
||||
);
|
||||
}
|
||||
|
||||
let json = str::from_utf8(&cue.stdout)?;
|
||||
codegen::generate(json, &generated)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "codegen"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Build-time codegen from FML JSON to Rust for fujicli"
|
||||
authors = [
|
||||
"Nikolaos Karaolidis <nick@karaolidis.com>",
|
||||
]
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
heck = "0.5.0"
|
||||
prettyplease = "0.2.37"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
@@ -0,0 +1,321 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ast::{LeafEquals, LeafPresent, Predicate};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Camera {
|
||||
pub id: String,
|
||||
pub spec: CameraSpec,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CameraSpec {
|
||||
pub name: String,
|
||||
pub generation: String,
|
||||
pub usb: Usb,
|
||||
pub features: Option<Features>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Usb {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub chunk_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Features {
|
||||
#[serde(default)]
|
||||
pub backup: bool,
|
||||
pub simulation: Option<Simulation>,
|
||||
pub render: Option<Render>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Simulation {
|
||||
pub slots: u32,
|
||||
pub settings: Vec<Setting>,
|
||||
#[serde(default)]
|
||||
pub transformations: Vec<Transformation>,
|
||||
#[serde(default)]
|
||||
pub rules: Vec<Rule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Render {
|
||||
pub profile_code: u32,
|
||||
pub fields: Vec<Field>,
|
||||
#[serde(default)]
|
||||
pub transformations: Vec<Transformation>,
|
||||
#[serde(default)]
|
||||
pub rules: Vec<Rule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Setting {
|
||||
pub id: String,
|
||||
pub r#ref: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum Field {
|
||||
Ref(FieldRef),
|
||||
Inline(FieldInline),
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Field::Ref(r) => &r.id,
|
||||
Field::Inline(i) => &i.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skip_read(&self) -> bool {
|
||||
match self {
|
||||
Field::Ref(r) => r.skip_read,
|
||||
Field::Inline(i) => i.skip_read,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skip_write(&self) -> bool {
|
||||
match self {
|
||||
Field::Ref(r) => r.skip_write,
|
||||
Field::Inline(i) => i.skip_write,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FieldRef {
|
||||
pub id: String,
|
||||
pub r#ref: String,
|
||||
#[serde(default)]
|
||||
pub skip_read: bool,
|
||||
#[serde(default)]
|
||||
pub skip_write: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FieldInline {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub skip_read: bool,
|
||||
#[serde(default)]
|
||||
pub skip_write: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SpecKind {
|
||||
Integer,
|
||||
Float,
|
||||
String,
|
||||
Enum,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Transformation {
|
||||
pub when: Option<Predicate>,
|
||||
pub apply: Vec<Assignment>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Assignment {
|
||||
pub r#ref: String,
|
||||
pub effect: AssignmentEffect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AssignmentEffect {
|
||||
Set(Value),
|
||||
Clear,
|
||||
}
|
||||
|
||||
impl From<&Assignment> for Predicate {
|
||||
fn from(a: &Assignment) -> Self {
|
||||
match &a.effect {
|
||||
AssignmentEffect::Set(v) => LeafEquals {
|
||||
r#ref: a.r#ref.clone(),
|
||||
equals: v.clone(),
|
||||
}
|
||||
.into(),
|
||||
AssignmentEffect::Clear => LeafPresent {
|
||||
r#ref: a.r#ref.clone(),
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Assignment {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Raw {
|
||||
r#ref: String,
|
||||
#[serde(default)]
|
||||
value: Option<Value>,
|
||||
#[serde(default)]
|
||||
present: Option<bool>,
|
||||
}
|
||||
|
||||
let raw = Raw::deserialize(deserializer)?;
|
||||
match (raw.value, raw.present) {
|
||||
(Some(value), None) => Ok(Assignment {
|
||||
r#ref: raw.r#ref,
|
||||
effect: AssignmentEffect::Set(value),
|
||||
}),
|
||||
(None, Some(false)) => Ok(Assignment {
|
||||
r#ref: raw.r#ref,
|
||||
effect: AssignmentEffect::Clear,
|
||||
}),
|
||||
_ => unreachable!("cue should ensure exactly one of the variants is present"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Rule {
|
||||
#[serde(default)]
|
||||
pub severity: Severity,
|
||||
pub message: String,
|
||||
pub when: Predicate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Severity {
|
||||
#[default]
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse<T: serde::de::DeserializeOwned>(json: &str) -> T {
|
||||
serde_json::from_str(json).unwrap()
|
||||
}
|
||||
|
||||
fn parse_err<T: serde::de::DeserializeOwned>(json: &str) {
|
||||
assert!(
|
||||
serde_json::from_str::<T>(json).is_err(),
|
||||
"expected parse error for: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_severity_defaults_to_error_and_round_trips() {
|
||||
let default_: Rule = parse(r#"{ "message": "x", "when": true }"#);
|
||||
let warn: Rule = parse(r#"{ "severity": "warning", "message": "x", "when": true }"#);
|
||||
let info: Rule = parse(r#"{ "severity": "info", "message": "x", "when": true }"#);
|
||||
let err: Rule = parse(r#"{ "severity": "error", "message": "x", "when": true }"#);
|
||||
assert_eq!(default_.severity, Severity::Error);
|
||||
assert_eq!(warn.severity, Severity::Warning);
|
||||
assert_eq!(info.severity, Severity::Info);
|
||||
assert_eq!(err.severity, Severity::Error);
|
||||
|
||||
parse_err::<Rule>(r#"{ "severity": "critical", "message": "x", "when": true }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_requires_ref() {
|
||||
let s: Setting = parse(r#"{ "id": "x", "ref": "image_size" }"#);
|
||||
assert_eq!(s.id, "x");
|
||||
assert_eq!(s.r#ref, "image_size");
|
||||
parse_err::<Setting>(r#"{ "id": "head_0" }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_disambiguates_ref_vs_inline_by_presence_of_ref() {
|
||||
let r: Field = parse(r#"{ "id": "x", "ref": "image_size" }"#);
|
||||
let i: Field = parse(r#"{ "id": "head_0" }"#);
|
||||
assert!(matches!(r, Field::Ref(_)));
|
||||
assert!(matches!(i, Field::Inline(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_skip_flags_default_false_and_round_trip() {
|
||||
let plain: Field = parse(r#"{ "id": "x", "ref": "image_size" }"#);
|
||||
let Field::Ref(plain) = plain else { panic!() };
|
||||
assert!(!plain.skip_read && !plain.skip_write);
|
||||
|
||||
let both: Field =
|
||||
parse(r#"{ "id": "x", "ref": "image_size", "skip_read": true, "skip_write": true }"#);
|
||||
let Field::Ref(both) = both else { panic!() };
|
||||
assert!(both.skip_read && both.skip_write);
|
||||
|
||||
let inline: Field = parse(r#"{ "id": "x", "skip_read": true }"#);
|
||||
let Field::Inline(inline) = inline else {
|
||||
panic!()
|
||||
};
|
||||
assert!(inline.skip_read && !inline.skip_write);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulation_transformations_and_rules_default_empty() {
|
||||
let sim: Simulation = parse(
|
||||
r#"{
|
||||
"slots": 4,
|
||||
"settings": [{ "id": "a", "ref": "image_size" }]
|
||||
}"#,
|
||||
);
|
||||
assert_eq!(sim.slots, 4);
|
||||
assert_eq!(sim.settings.len(), 1);
|
||||
assert!(sim.transformations.is_empty());
|
||||
assert!(sim.rules.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transformation_when_is_optional() {
|
||||
let t: Transformation = parse(r#"{ "apply": [{ "ref": "x", "value": 0 }] }"#);
|
||||
assert!(t.when.is_none());
|
||||
assert_eq!(t.apply.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assignment_parses_set_form() {
|
||||
let a: Assignment = parse(r#"{ "ref": "x", "value": 5 }"#);
|
||||
assert_eq!(a.r#ref, "x");
|
||||
assert!(matches!(a.effect, AssignmentEffect::Set(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assignment_parses_clear_form() {
|
||||
let a: Assignment = parse(r#"{ "ref": "x", "present": false }"#);
|
||||
assert_eq!(a.r#ref, "x");
|
||||
assert!(matches!(a.effect, AssignmentEffect::Clear));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn features_backup_defaults_false_and_capabilities_optional() {
|
||||
let f: Features = parse("{}");
|
||||
assert!(!f.backup);
|
||||
assert!(f.simulation.is_none() && f.render.is_none());
|
||||
|
||||
let only_render: Features = parse(r#"{ "render": { "profile_code": 1, "fields": [] } }"#);
|
||||
assert!(!only_render.backup);
|
||||
assert!(only_render.simulation.is_none());
|
||||
assert!(only_render.render.is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Deref, DerefMut},
|
||||
slice::Iter,
|
||||
vec::IntoIter,
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::{
|
||||
ast::{
|
||||
Assignment, AssignmentEffect, LeafBetween, LeafEquals, LeafGt, LeafGte, LeafIn, LeafLt,
|
||||
LeafLte, LeafPresent, PredAll, PredAny, PredNot, Predicate,
|
||||
},
|
||||
util::multiset,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Leaf {
|
||||
Present(LeafPresent),
|
||||
Equals(LeafEquals),
|
||||
NotEquals(LeafEquals),
|
||||
In(LeafIn),
|
||||
NotIn(LeafIn),
|
||||
Between(LeafBetween),
|
||||
NotBetween(LeafBetween),
|
||||
LessThan(LeafLt),
|
||||
NotLessThan(LeafLt),
|
||||
LessThanOrEqual(LeafLte),
|
||||
NotLessThanOrEqual(LeafLte),
|
||||
GreaterThan(LeafGt),
|
||||
NotGreaterThan(LeafGt),
|
||||
GreaterThanOrEqual(LeafGte),
|
||||
NotGreaterThanOrEqual(LeafGte),
|
||||
}
|
||||
|
||||
impl Leaf {
|
||||
pub fn r#ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Present(p) => &p.r#ref,
|
||||
Self::Equals(p) | Self::NotEquals(p) => &p.r#ref,
|
||||
Self::In(p) | Self::NotIn(p) => &p.r#ref,
|
||||
Self::Between(p) | Self::NotBetween(p) => &p.r#ref,
|
||||
Self::LessThan(p) | Self::NotLessThan(p) => &p.r#ref,
|
||||
Self::LessThanOrEqual(p) | Self::NotLessThanOrEqual(p) => &p.r#ref,
|
||||
Self::GreaterThan(p) | Self::NotGreaterThan(p) => &p.r#ref,
|
||||
Self::GreaterThanOrEqual(p) | Self::NotGreaterThanOrEqual(p) => &p.r#ref,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negated(self) -> Self {
|
||||
match self {
|
||||
Self::Present(LeafPresent { r#ref, present }) => Self::Present(LeafPresent {
|
||||
r#ref,
|
||||
present: !present,
|
||||
}),
|
||||
Self::Equals(l) => Self::NotEquals(l),
|
||||
Self::NotEquals(l) => Self::Equals(l),
|
||||
Self::In(l) => Self::NotIn(l),
|
||||
Self::NotIn(l) => Self::In(l),
|
||||
Self::Between(l) => Self::NotBetween(l),
|
||||
Self::NotBetween(l) => Self::Between(l),
|
||||
Self::LessThan(l) => Self::NotLessThan(l),
|
||||
Self::NotLessThan(l) => Self::LessThan(l),
|
||||
Self::LessThanOrEqual(l) => Self::NotLessThanOrEqual(l),
|
||||
Self::NotLessThanOrEqual(l) => Self::LessThanOrEqual(l),
|
||||
Self::GreaterThan(l) => Self::NotGreaterThan(l),
|
||||
Self::NotGreaterThan(l) => Self::GreaterThan(l),
|
||||
Self::GreaterThanOrEqual(l) => Self::NotGreaterThanOrEqual(l),
|
||||
Self::NotGreaterThanOrEqual(l) => Self::GreaterThanOrEqual(l),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Assignment> for Leaf {
|
||||
fn from(a: &Assignment) -> Self {
|
||||
match &a.effect {
|
||||
AssignmentEffect::Set(v) => Leaf::Equals(LeafEquals {
|
||||
r#ref: a.r#ref.clone(),
|
||||
equals: v.clone(),
|
||||
}),
|
||||
AssignmentEffect::Clear => Leaf::Present(LeafPresent {
|
||||
r#ref: a.r#ref.clone(),
|
||||
present: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Leaf> for Predicate {
|
||||
fn from(l: Leaf) -> Self {
|
||||
match l {
|
||||
Leaf::Present(p) => p.into(),
|
||||
Leaf::Equals(p) => p.into(),
|
||||
Leaf::NotEquals(p) => {
|
||||
let p = p.into();
|
||||
PredNot { not: Box::new(p) }.into()
|
||||
}
|
||||
Leaf::In(p) => p.into(),
|
||||
Leaf::NotIn(p) => PredNot {
|
||||
not: Box::new(p.into()),
|
||||
}
|
||||
.into(),
|
||||
Leaf::Between(p) => p.into(),
|
||||
Leaf::NotBetween(p) => {
|
||||
let p = p.into();
|
||||
PredNot { not: Box::new(p) }.into()
|
||||
}
|
||||
Leaf::LessThan(p) => p.into(),
|
||||
Leaf::NotLessThan(p) => {
|
||||
let p = p.into();
|
||||
PredNot { not: Box::new(p) }.into()
|
||||
}
|
||||
Leaf::LessThanOrEqual(p) => p.into(),
|
||||
Leaf::NotLessThanOrEqual(p) => {
|
||||
let p = p.into();
|
||||
PredNot { not: Box::new(p) }.into()
|
||||
}
|
||||
Leaf::GreaterThan(p) => p.into(),
|
||||
Leaf::NotGreaterThan(p) => {
|
||||
let p = p.into();
|
||||
PredNot { not: Box::new(p) }.into()
|
||||
}
|
||||
Leaf::GreaterThanOrEqual(p) => p.into(),
|
||||
Leaf::NotGreaterThanOrEqual(p) => {
|
||||
let p = p.into();
|
||||
PredNot { not: Box::new(p) }.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq)]
|
||||
pub struct Conjunction(pub Vec<Leaf>);
|
||||
|
||||
impl Conjunction {
|
||||
pub fn refs(&self, out: &mut BTreeSet<String>) {
|
||||
self.iter().for_each(|leaf| {
|
||||
out.insert(leaf.r#ref().to_string());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn contains_all(&self, needle: &Conjunction) -> bool {
|
||||
multiset::subset(needle, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Conjunction {
|
||||
type Target = Vec<Leaf>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Conjunction {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Conjunction {
|
||||
type Item = &'a Leaf;
|
||||
type IntoIter = Iter<'a, Leaf>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Conjunction {
|
||||
type Item = Leaf;
|
||||
type IntoIter = IntoIter<Leaf>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Conjunction {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
multiset::eq(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Conjunction {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
multiset::hash(self, state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq)]
|
||||
pub struct Dnf(pub Vec<Conjunction>);
|
||||
|
||||
impl Dnf {
|
||||
pub fn is_tautology(&self) -> bool {
|
||||
self.iter().any(|c| c.is_empty())
|
||||
}
|
||||
|
||||
pub fn is_contradiction(&self) -> bool {
|
||||
self.is_empty()
|
||||
}
|
||||
|
||||
pub fn refs(&self, out: &mut BTreeSet<String>) {
|
||||
self.iter().for_each(|c| c.refs(out));
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Dnf {
|
||||
type Target = Vec<Conjunction>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Dnf {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Dnf {
|
||||
type Item = &'a Conjunction;
|
||||
type IntoIter = Iter<'a, Conjunction>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Dnf {
|
||||
type Item = Conjunction;
|
||||
type IntoIter = IntoIter<Conjunction>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Dnf {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
multiset::eq(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Dnf {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
multiset::hash(self, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Predicate> for Dnf {
|
||||
fn from(p: Predicate) -> Self {
|
||||
Dnf(p.into_disjuncts())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dnf> for Predicate {
|
||||
fn from(dnf: Dnf) -> Self {
|
||||
if dnf.is_contradiction() {
|
||||
return Predicate::Bool(false);
|
||||
}
|
||||
|
||||
if dnf.is_tautology() {
|
||||
return Predicate::Bool(true);
|
||||
}
|
||||
|
||||
let mut disjuncts: Vec<Predicate> = dnf
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
if c.len() == 1 {
|
||||
Predicate::from(
|
||||
c.into_iter()
|
||||
.next()
|
||||
.expect("conjunction has exactly one element"),
|
||||
)
|
||||
} else {
|
||||
PredAll {
|
||||
all: c.into_iter().map(Predicate::from).collect(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if disjuncts.len() == 1 {
|
||||
disjuncts.pop().expect("disjuncts has exactly one element")
|
||||
} else {
|
||||
PredAny { any: disjuncts }.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Predicate {
|
||||
pub fn into_disjuncts(self) -> Vec<Conjunction> {
|
||||
match self {
|
||||
Predicate::All(a) => {
|
||||
a.all
|
||||
.into_iter()
|
||||
.fold(vec![Conjunction(Vec::new())], |acc, clause| {
|
||||
if acc.is_empty() {
|
||||
return acc;
|
||||
}
|
||||
|
||||
let parts = clause.into_disjuncts();
|
||||
if parts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
acc.iter()
|
||||
.flat_map(|prefix| {
|
||||
parts.iter().map(move |part| {
|
||||
Conjunction(
|
||||
prefix
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(part.iter().cloned())
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
Predicate::Any(a) => a.any.into_iter().flat_map(|p| p.into_disjuncts()).collect(),
|
||||
Predicate::Not(n) => Self::into_negation(*n.not),
|
||||
Predicate::Bool(true) => vec![Conjunction(Vec::new())],
|
||||
Predicate::Bool(false) => Vec::new(),
|
||||
leaf => {
|
||||
let leaf_val =
|
||||
Leaf::try_from(leaf).expect("into_leaf called on non-leaf predicate");
|
||||
vec![Conjunction(vec![leaf_val])]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_negation(p: Predicate) -> Vec<Conjunction> {
|
||||
match p {
|
||||
Predicate::All(a) => Predicate::Any(PredAny {
|
||||
any: a
|
||||
.all
|
||||
.into_iter()
|
||||
.map(|c| PredNot { not: Box::new(c) }.into())
|
||||
.collect(),
|
||||
})
|
||||
.into_disjuncts(),
|
||||
Predicate::Any(a) => Predicate::All(PredAll {
|
||||
all: a
|
||||
.any
|
||||
.into_iter()
|
||||
.map(|c| PredNot { not: Box::new(c) }.into())
|
||||
.collect(),
|
||||
})
|
||||
.into_disjuncts(),
|
||||
Predicate::Not(inner) => inner.not.into_disjuncts(),
|
||||
Predicate::Bool(b) => {
|
||||
if b {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![Conjunction(Vec::new())]
|
||||
}
|
||||
}
|
||||
leaf => {
|
||||
let leaf_val =
|
||||
Leaf::try_from(leaf).expect("into_leaf called on non-leaf predicate");
|
||||
vec![Conjunction(vec![leaf_val.negated()])]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Predicate> for Leaf {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(p: Predicate) -> Result<Self, Self::Error> {
|
||||
match p {
|
||||
Predicate::Present(p) => Ok(Leaf::Present(p)),
|
||||
Predicate::Equals(p) => Ok(Leaf::Equals(p)),
|
||||
Predicate::In(p) => Ok(Leaf::In(p)),
|
||||
Predicate::Between(p) => Ok(Leaf::Between(p)),
|
||||
Predicate::LessThan(p) => Ok(Leaf::LessThan(p)),
|
||||
Predicate::LessThanOrEqual(p) => Ok(Leaf::LessThanOrEqual(p)),
|
||||
Predicate::GreaterThan(p) => Ok(Leaf::GreaterThan(p)),
|
||||
Predicate::GreaterThanOrEqual(p) => Ok(Leaf::GreaterThanOrEqual(p)),
|
||||
Predicate::All(_) | Predicate::Any(_) | Predicate::Not(_) | Predicate::Bool(_) => {
|
||||
bail!("into_leaf called on non-leaf predicate")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn lp(name: &str, present: bool) -> Leaf {
|
||||
Leaf::Present(LeafPresent {
|
||||
r#ref: name.into(),
|
||||
present,
|
||||
})
|
||||
}
|
||||
|
||||
fn le(name: &str, v: serde_json::Value) -> Leaf {
|
||||
Leaf::Equals(LeafEquals {
|
||||
r#ref: name.into(),
|
||||
equals: v,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_negation_round_trips() {
|
||||
let l = le("x", json!(1));
|
||||
assert_eq!(l.clone().negated().negated(), l);
|
||||
let p = lp("y", true);
|
||||
assert_eq!(p.clone().negated().negated(), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_dnf_bool_true_is_tautology() {
|
||||
assert!(Dnf::from(Predicate::Bool(true)).is_tautology());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_dnf_bool_false_is_contradiction() {
|
||||
assert!(Dnf::from(Predicate::Bool(false)).is_contradiction());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_dnf_empty_all_is_tautology() {
|
||||
let dnf = Dnf::from(Predicate::from(PredAll { all: vec![] }));
|
||||
assert!(dnf.is_tautology());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_dnf_empty_any_is_contradiction() {
|
||||
let dnf = Dnf::from(Predicate::from(PredAny { any: vec![] }));
|
||||
assert!(dnf.is_contradiction());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_equals_becomes_dedicated_literal() {
|
||||
let dnf = Dnf::from(Predicate::from(PredNot {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}));
|
||||
|
||||
assert_eq!(dnf.len(), 1);
|
||||
assert_eq!(dnf[0].len(), 1);
|
||||
assert!(matches!(dnf[0][0], Leaf::NotEquals(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_present_flips_polarity_in_place() {
|
||||
let dnf = Dnf::from(Predicate::from(PredNot {
|
||||
not: Box::new(
|
||||
LeafPresent {
|
||||
r#ref: "a".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}));
|
||||
assert_eq!(dnf[0][0], lp("a", false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_negation_cancels() {
|
||||
let dnf = Dnf::from(Predicate::from(PredNot {
|
||||
not: Box::new(
|
||||
PredNot {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}));
|
||||
assert_eq!(dnf[0][0], le("a", json!(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distribution_across_inner_any() {
|
||||
let dnf = Dnf::from(Predicate::from(PredAll {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "A".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
PredAny {
|
||||
any: vec![
|
||||
LeafEquals {
|
||||
r#ref: "B".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}));
|
||||
assert_eq!(dnf.len(), 2);
|
||||
for c in dnf {
|
||||
assert_eq!(c.len(), 2);
|
||||
assert!(c.iter().any(|l| *l == lp("A", true)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conjunction_eq_is_multiset() {
|
||||
let a = Conjunction(vec![le("x", json!(1)), lp("y", true)]);
|
||||
let b = Conjunction(vec![lp("y", true), le("x", json!(1))]);
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conjunction_contains_all_respects_multiset() {
|
||||
let haystack = Conjunction(vec![le("x", json!(1)), lp("y", true), le("x", json!(2))]);
|
||||
let needle_ok = Conjunction(vec![le("x", json!(1))]);
|
||||
let needle_dup = Conjunction(vec![le("x", json!(1)), le("x", json!(1))]);
|
||||
let needle_missing = Conjunction(vec![le("z", json!(0))]);
|
||||
assert!(haystack.contains_all(&needle_ok));
|
||||
assert!(!haystack.contains_all(&needle_dup));
|
||||
assert!(!haystack.contains_all(&needle_missing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_predicate_to_dnf_and_back_is_logically_equivalent() {
|
||||
let p: Predicate = PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
PredAny {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "C".into(),
|
||||
present: false,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into();
|
||||
let dnf = Dnf::from(p.clone());
|
||||
let p2: Predicate = dnf.clone().into();
|
||||
assert_eq!(Dnf::from(p2), dnf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn de_morgan_not_all_becomes_any_of_negated_literals() {
|
||||
let dnf = Dnf::from(Predicate::from(PredNot {
|
||||
not: Box::new(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}));
|
||||
assert_eq!(dnf.len(), 2);
|
||||
let flat: Vec<&Leaf> = dnf.iter().flat_map(|c| c.iter()).collect();
|
||||
assert!(flat.iter().any(|l| matches!(l, Leaf::NotEquals(_))));
|
||||
assert!(
|
||||
flat.iter()
|
||||
.any(|l| matches!(l, Leaf::Present(p) if !p.present))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Generation {
|
||||
pub id: String,
|
||||
pub spec: GenerationSpec,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GenerationSpec {
|
||||
pub name: String,
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Deref, DerefMut},
|
||||
slice::Iter,
|
||||
vec::IntoIter,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::util::multiset;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum Predicate {
|
||||
Bool(bool),
|
||||
All(PredAll),
|
||||
Any(PredAny),
|
||||
Not(PredNot),
|
||||
Equals(LeafEquals),
|
||||
In(LeafIn),
|
||||
Between(LeafBetween),
|
||||
LessThan(LeafLt),
|
||||
LessThanOrEqual(LeafLte),
|
||||
GreaterThan(LeafGt),
|
||||
GreaterThanOrEqual(LeafGte),
|
||||
Present(LeafPresent),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PredAll {
|
||||
pub all: Vec<Predicate>,
|
||||
}
|
||||
|
||||
impl Deref for PredAll {
|
||||
type Target = Vec<Predicate>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.all
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PredAll {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.all
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a PredAll {
|
||||
type Item = &'a Predicate;
|
||||
type IntoIter = Iter<'a, Predicate>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.all.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for PredAll {
|
||||
type Item = Predicate;
|
||||
type IntoIter = IntoIter<Predicate>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.all.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PredAll {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
multiset::eq(&self.all, &other.all)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for PredAll {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
multiset::hash(&self.all, state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PredAny {
|
||||
pub any: Vec<Predicate>,
|
||||
}
|
||||
|
||||
impl Deref for PredAny {
|
||||
type Target = Vec<Predicate>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.any
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PredAny {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.any
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a PredAny {
|
||||
type Item = &'a Predicate;
|
||||
type IntoIter = Iter<'a, Predicate>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.any.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for PredAny {
|
||||
type Item = Predicate;
|
||||
type IntoIter = IntoIter<Predicate>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.any.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PredAny {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
multiset::eq(&self.any, &other.any)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for PredAny {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
multiset::hash(&self.any, state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PredNot {
|
||||
pub not: Box<Predicate>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafEquals {
|
||||
pub r#ref: String,
|
||||
pub equals: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafIn {
|
||||
pub r#ref: String,
|
||||
#[serde(rename = "in")]
|
||||
pub values: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafBetween {
|
||||
pub r#ref: String,
|
||||
pub min: Value,
|
||||
pub max: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafLt {
|
||||
pub r#ref: String,
|
||||
pub lt: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafLte {
|
||||
pub r#ref: String,
|
||||
pub lte: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafGt {
|
||||
pub r#ref: String,
|
||||
pub gt: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafGte {
|
||||
pub r#ref: String,
|
||||
pub gte: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LeafPresent {
|
||||
pub r#ref: String,
|
||||
pub present: bool,
|
||||
}
|
||||
|
||||
macro_rules! impl_into_predicate {
|
||||
($($variant:ident($inner:ty)),* $(,)?) => {
|
||||
$(
|
||||
impl From<$inner> for Predicate {
|
||||
fn from(value: $inner) -> Self {
|
||||
Predicate::$variant(value)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_into_predicate!(
|
||||
All(PredAll),
|
||||
Any(PredAny),
|
||||
Not(PredNot),
|
||||
Equals(LeafEquals),
|
||||
In(LeafIn),
|
||||
Between(LeafBetween),
|
||||
LessThan(LeafLt),
|
||||
LessThanOrEqual(LeafLte),
|
||||
GreaterThan(LeafGt),
|
||||
GreaterThanOrEqual(LeafGte),
|
||||
Present(LeafPresent),
|
||||
);
|
||||
|
||||
impl Predicate {
|
||||
pub fn collect<T: Ord>(&self, out: &mut BTreeSet<T>, f: &impl Fn(&Predicate) -> Option<T>) {
|
||||
match self {
|
||||
Predicate::All(p) => p.all.iter().for_each(|c| c.collect(out, f)),
|
||||
Predicate::Any(p) => p.any.iter().for_each(|c| c.collect(out, f)),
|
||||
Predicate::Not(p) => p.not.collect(out, f),
|
||||
leaf => {
|
||||
if let Some(v) = f(leaf) {
|
||||
out.insert(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refs(&self, out: &mut BTreeSet<String>) {
|
||||
self.collect(out, &|p| match p {
|
||||
Predicate::Present(p) => Some(p.r#ref.clone()),
|
||||
Predicate::Equals(p) => Some(p.r#ref.clone()),
|
||||
Predicate::In(p) => Some(p.r#ref.clone()),
|
||||
Predicate::Between(p) => Some(p.r#ref.clone()),
|
||||
Predicate::LessThan(p) => Some(p.r#ref.clone()),
|
||||
Predicate::LessThanOrEqual(p) => Some(p.r#ref.clone()),
|
||||
Predicate::GreaterThan(p) => Some(p.r#ref.clone()),
|
||||
Predicate::GreaterThanOrEqual(p) => Some(p.r#ref.clone()),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn parse(json: &str) -> Predicate {
|
||||
serde_json::from_str(json).unwrap()
|
||||
}
|
||||
|
||||
fn parse_err(json: &str) {
|
||||
assert!(
|
||||
serde_json::from_str::<Predicate>(json).is_err(),
|
||||
"expected parse error for: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_logic_round_trips() {
|
||||
let pred = parse(
|
||||
r#"{ "all": [
|
||||
{ "ref": "x", "equals": 5 },
|
||||
{ "not": { "ref": "y", "present": true } }
|
||||
] }"#,
|
||||
);
|
||||
assert!(matches!(pred, Predicate::All(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bool_literal_parses_both_polarities() {
|
||||
assert!(matches!(parse("true"), Predicate::Bool(true)));
|
||||
assert!(matches!(parse("false"), Predicate::Bool(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_all_and_any_arrays_parse() {
|
||||
assert!(matches!(parse(r#"{ "all": [] }"#), Predicate::All(_)));
|
||||
assert!(matches!(parse(r#"{ "any": [] }"#), Predicate::Any(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_is_recursively_boxed() {
|
||||
let pred = parse(r#"{ "not": { "not": { "ref": "x", "present": true } } }"#);
|
||||
let Predicate::Not(outer) = pred else {
|
||||
panic!()
|
||||
};
|
||||
let Predicate::Not(inner) = *outer.not else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(*inner.not, Predicate::Present(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparator_leaves_disambiguate_by_key_name() {
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "lt": 5 }"#),
|
||||
Predicate::LessThan(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "lte": 5 }"#),
|
||||
Predicate::LessThanOrEqual(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "gt": 5 }"#),
|
||||
Predicate::GreaterThan(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(r#"{ "ref": "x", "gte": 5 }"#),
|
||||
Predicate::GreaterThanOrEqual(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_accepts_any_json_scalar() {
|
||||
let eq: Predicate = parse(r#"{ "ref": "x", "equals": "hello" }"#);
|
||||
let Predicate::Equals(leaf) = eq else {
|
||||
panic!()
|
||||
};
|
||||
assert!(leaf.equals.is_string());
|
||||
|
||||
let eq: Predicate = parse(r#"{ "ref": "x", "equals": true }"#);
|
||||
let Predicate::Equals(leaf) = eq else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(leaf.equals.as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn between_requires_both_bounds() {
|
||||
parse_err(r#"{ "ref": "x", "min": 5 }"#);
|
||||
parse_err(r#"{ "ref": "x", "max": 5 }"#);
|
||||
let bt = parse(r#"{ "ref": "x", "min": 0, "max": 10 }"#);
|
||||
assert!(matches!(bt, Predicate::Between(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_accepts_both_polarities() {
|
||||
let t = parse(r#"{ "ref": "x", "present": true }"#);
|
||||
let f = parse(r#"{ "ref": "x", "present": false }"#);
|
||||
let Predicate::Present(t) = t else { panic!() };
|
||||
let Predicate::Present(f) = f else { panic!() };
|
||||
assert!(t.present);
|
||||
assert!(!f.present);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixing_logic_keys_is_rejected() {
|
||||
parse_err(r#"{ "all": [], "any": [] }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_required_on_all_leaves() {
|
||||
parse_err(r#"{ "equals": 5 }"#);
|
||||
parse_err(r#"{ "present": true }"#);
|
||||
parse_err(r#"{ "min": 0, "max": 10 }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_refs_leaves() {
|
||||
let mut out = BTreeSet::new();
|
||||
Predicate::from(LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
equals: json!("x"),
|
||||
})
|
||||
.refs(&mut out);
|
||||
Predicate::from(LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
present: true,
|
||||
})
|
||||
.refs(&mut out);
|
||||
Predicate::from(LeafIn {
|
||||
r#ref: "C".into(),
|
||||
values: vec![json!(1), json!(2)],
|
||||
})
|
||||
.refs(&mut out);
|
||||
assert_eq!(out, ["A", "B", "C"].iter().map(|s| s.to_string()).collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_refs_nested() {
|
||||
let mut out = BTreeSet::new();
|
||||
Predicate::from(PredAll {
|
||||
all: vec![
|
||||
PredNot {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "A".into(),
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
PredAny {
|
||||
any: vec![
|
||||
LeafPresent {
|
||||
r#ref: "B".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "C".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
})
|
||||
.refs(&mut out);
|
||||
assert_eq!(out, ["A", "B", "C"].iter().map(|s| s.to_string()).collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_refs_bool_has_no_refs() {
|
||||
let mut out = BTreeSet::new();
|
||||
Predicate::Bool(true).refs(&mut out);
|
||||
Predicate::Bool(false).refs(&mut out);
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
mod cameras;
|
||||
mod dnf;
|
||||
mod generations;
|
||||
mod grammar;
|
||||
mod options;
|
||||
|
||||
pub use cameras::*;
|
||||
pub use dnf::*;
|
||||
pub use generations::*;
|
||||
pub use grammar::*;
|
||||
pub use options::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Fml {
|
||||
pub cameras: BTreeMap<String, Camera>,
|
||||
pub options: BTreeMap<String, FujiOption>,
|
||||
pub generations: BTreeMap<String, Generation>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(json: &str) -> Fml {
|
||||
serde_json::from_str(json).unwrap()
|
||||
}
|
||||
|
||||
fn parse_err(json: &str) {
|
||||
assert!(
|
||||
serde_json::from_str::<Fml>(json).is_err(),
|
||||
"expected parse error for: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_empty_fml() {
|
||||
let fml = parse(r#"{ "cameras": {}, "options": {}, "generations": {} }"#);
|
||||
assert!(fml.cameras.is_empty() && fml.options.is_empty() && fml.generations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_top_level_field_required() {
|
||||
parse_err(r#"{ "options": {}, "generations": {} }"#);
|
||||
parse_err(r#"{ "cameras": {}, "generations": {} }"#);
|
||||
parse_err(r#"{ "cameras": {}, "options": {} }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_full_minimal_camera_with_simulation() {
|
||||
let fml = parse(
|
||||
r#"{
|
||||
"cameras": {
|
||||
"demo": {
|
||||
"id": "demo",
|
||||
"spec": {
|
||||
"name": "Demo",
|
||||
"generation": "gen_a",
|
||||
"usb": { "vendor_id": 1, "product_id": 2, "chunk_size": 1024 },
|
||||
"features": {
|
||||
"simulation": {
|
||||
"slots": 1,
|
||||
"settings": [{ "id": "img", "ref": "image_size" }],
|
||||
"rules": [{
|
||||
"message": "demo rule",
|
||||
"when": { "ref": "img", "present": true }
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"image_size": {
|
||||
"id": "image_size",
|
||||
"spec": {
|
||||
"name": "Image Size", "kind": "enum",
|
||||
"rules": { "variants": [
|
||||
{ "id": "small", "name": "Small", "aliases": ["s"] }
|
||||
] },
|
||||
"encoding": { "kind": "lookup", "spec": { "values": { "small": 1 } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"generations": {
|
||||
"gen_a": { "id": "gen_a", "spec": { "name": "Gen A" } }
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
assert_eq!(fml.cameras["demo"].spec.generation, "gen_a");
|
||||
assert_eq!(fml.options["image_size"].spec.name(), "Image Size");
|
||||
assert_eq!(fml.generations["gen_a"].spec.name, "Gen A");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ast::SpecKind;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FujiOption {
|
||||
pub id: String,
|
||||
pub spec: OptionSpec,
|
||||
#[serde(default)]
|
||||
pub codegen: Codegen,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Codegen {
|
||||
pub skip_args: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
|
||||
pub enum OptionSpec {
|
||||
Integer {
|
||||
name: String,
|
||||
rules: Option<NumericRules<i32>>,
|
||||
encoding: NumericEncoding,
|
||||
},
|
||||
Float {
|
||||
name: String,
|
||||
rules: Option<NumericRules<f32>>,
|
||||
encoding: NumericEncoding,
|
||||
},
|
||||
String {
|
||||
name: String,
|
||||
rules: Option<StringRules>,
|
||||
encoding: StringEncoding,
|
||||
},
|
||||
Enum {
|
||||
name: String,
|
||||
rules: EnumRules,
|
||||
encoding: EnumEncoding,
|
||||
},
|
||||
}
|
||||
|
||||
impl OptionSpec {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Integer { name, .. }
|
||||
| Self::Float { name, .. }
|
||||
| Self::String { name, .. }
|
||||
| Self::Enum { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> SpecKind {
|
||||
match &self {
|
||||
OptionSpec::Integer { .. } => SpecKind::Integer,
|
||||
OptionSpec::Float { .. } => SpecKind::Float,
|
||||
OptionSpec::String { .. } => SpecKind::String,
|
||||
OptionSpec::Enum { .. } => SpecKind::Enum,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prop_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Integer { encoding, .. } | Self::Float { encoding, .. } => encoding.prop_code(),
|
||||
Self::String { encoding, .. } => encoding.prop_code(),
|
||||
Self::Enum { encoding, .. } => encoding.prop_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, bound = "T: Deserialize<'de>")]
|
||||
pub struct NumericRules<T> {
|
||||
pub min: Option<T>,
|
||||
pub max: Option<T>,
|
||||
pub step: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StringRules {
|
||||
pub min_length: Option<u32>,
|
||||
pub max_length: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnumRules {
|
||||
pub variants: Vec<EnumVariant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EnumVariant {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub aliases: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
|
||||
pub enum NumericEncoding {
|
||||
Raw {
|
||||
prop_code: Option<u16>,
|
||||
},
|
||||
Scale {
|
||||
prop_code: Option<u16>,
|
||||
spec: ScaleSpec,
|
||||
},
|
||||
Lookup {
|
||||
prop_code: Option<u16>,
|
||||
spec: LookupSpec,
|
||||
},
|
||||
}
|
||||
|
||||
impl NumericEncoding {
|
||||
pub fn prop_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Raw { prop_code }
|
||||
| Self::Scale { prop_code, .. }
|
||||
| Self::Lookup { prop_code, .. } => *prop_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
|
||||
pub enum StringEncoding {
|
||||
Raw { prop_code: Option<u16> },
|
||||
}
|
||||
|
||||
impl StringEncoding {
|
||||
pub fn prop_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Raw { prop_code } => *prop_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
|
||||
pub enum EnumEncoding {
|
||||
Lookup {
|
||||
prop_code: Option<u16>,
|
||||
spec: LookupSpec,
|
||||
},
|
||||
}
|
||||
|
||||
impl EnumEncoding {
|
||||
pub fn prop_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Lookup { prop_code, .. } => *prop_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ScaleSpec {
|
||||
pub scale: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LookupSpec {
|
||||
pub values: BTreeMap<String, LookupValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum LookupValue {
|
||||
Single(i32),
|
||||
Multi(Vec<i32>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_option(json: &str) -> FujiOption {
|
||||
serde_json::from_str(json).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_value_single_vs_multi() {
|
||||
let opt = parse_option(
|
||||
r#"{
|
||||
"id": "x", "spec": {
|
||||
"name": "X", "kind": "integer", "rules": { "min": 0, "max": 1 },
|
||||
"encoding": { "kind": "lookup", "spec": { "values": {
|
||||
"a": 5,
|
||||
"b": [1, 2, 3]
|
||||
} } }
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
let OptionSpec::Integer { encoding, .. } = opt.spec else {
|
||||
panic!()
|
||||
};
|
||||
let NumericEncoding::Lookup { spec, .. } = encoding else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(spec.values["a"], LookupValue::Single(5)));
|
||||
match &spec.values["b"] {
|
||||
LookupValue::Multi(v) => assert_eq!(v, &[1, 2, 3]),
|
||||
_ => panic!("expected Multi for [1,2,3]"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codegen_block_defaults_to_skip_args_false() {
|
||||
let opt = parse_option(
|
||||
r#"{
|
||||
"id": "x", "spec": {
|
||||
"name": "X", "kind": "integer", "encoding": { "kind": "raw" }
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
assert!(!opt.codegen.skip_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_helpers_report_consistently_across_variants() {
|
||||
let int = parse_option(
|
||||
r#"{ "id": "i", "spec": { "name": "I", "kind": "integer", "encoding": { "kind": "raw" } } }"#,
|
||||
);
|
||||
let flt = parse_option(
|
||||
r#"{ "id": "f", "spec": { "name": "F", "kind": "float", "encoding": { "kind": "scale", "spec": { "scale": 10 } } } }"#,
|
||||
);
|
||||
let s = parse_option(
|
||||
r#"{ "id": "s", "spec": { "name": "S", "kind": "string", "encoding": { "kind": "raw" } } }"#,
|
||||
);
|
||||
let e = parse_option(
|
||||
r#"{ "id": "e", "spec": { "name": "E", "kind": "enum", "rules": { "variants": [] }, "encoding": { "kind": "lookup", "spec": { "values": {} } } } }"#,
|
||||
);
|
||||
assert_eq!(int.spec.kind(), crate::ast::SpecKind::Integer);
|
||||
assert_eq!(flt.spec.kind(), crate::ast::SpecKind::Float);
|
||||
assert_eq!(s.spec.kind(), crate::ast::SpecKind::String);
|
||||
assert_eq!(e.spec.kind(), crate::ast::SpecKind::Enum);
|
||||
assert_eq!(int.spec.name(), "I");
|
||||
assert_eq!(e.spec.name(), "E");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
pub mod render;
|
||||
pub mod simulation;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::ast::{Camera, FujiOption};
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let simulation = simulation::generate(options, cameras).context("generating SimulationArgs")?;
|
||||
let render = render::generate(options, cameras).context("generating RenderArgs")?;
|
||||
|
||||
Ok(quote! {
|
||||
//! Generated CLI types. Do not edit.
|
||||
|
||||
#simulation
|
||||
#render
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::{
|
||||
ast::{Camera, Field, FujiOption},
|
||||
common::{options, renders},
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
ident: proc_macro2::Ident,
|
||||
type_path: TokenStream,
|
||||
}
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let render_options = collect_render_option_ids(cameras);
|
||||
let render_inline_ids = collect_render_inline_ids(cameras);
|
||||
let entries = build_entries(options, &render_options, &render_inline_ids);
|
||||
|
||||
let struct_def = generate_struct(&entries);
|
||||
let from_impl = generate_from_impl(&entries);
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#from_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_render_option_ids(cameras: &BTreeMap<String, Camera>) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
for camera in cameras.values() {
|
||||
let Some(render) = camera
|
||||
.spec
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|f| f.render.as_ref())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for field in &render.fields {
|
||||
if let Field::Ref(r) = field {
|
||||
out.insert(r.r#ref.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn collect_render_inline_ids(cameras: &BTreeMap<String, Camera>) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
for camera in cameras.values() {
|
||||
let Some(render) = camera
|
||||
.spec
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|f| f.render.as_ref())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for field in &render.fields {
|
||||
if let Field::Inline(i) = field {
|
||||
out.insert(i.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn build_entries(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
render_options: &BTreeSet<String>,
|
||||
render_inline_ids: &BTreeSet<String>,
|
||||
) -> Vec<Entry> {
|
||||
options
|
||||
.values()
|
||||
.filter(|opt| !opt.codegen.skip_args)
|
||||
.filter(|opt| render_options.contains(&opt.id) && !render_inline_ids.contains(&opt.id))
|
||||
.map(|opt| {
|
||||
let ident = format_ident!("{}", opt.id);
|
||||
let type_ident = safe_upper_camel_case_ident(&opt.id);
|
||||
let options_path = options::path();
|
||||
let type_path = quote! { #options_path::#type_ident };
|
||||
Entry { ident, type_path }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_struct(entries: &[Entry]) -> TokenStream {
|
||||
let fields = entries.iter().map(|entry| {
|
||||
let ident = &entry.ident;
|
||||
let ty = &entry.type_path;
|
||||
let attrs = quote! { #[clap(long, allow_hyphen_values(true))] };
|
||||
quote! {
|
||||
#attrs
|
||||
pub #ident: Option<#ty>,
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(::clap::Args, Debug, Default, Clone)]
|
||||
pub struct RenderArgs {
|
||||
#( #fields )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_from_impl(entries: &[Entry]) -> TokenStream {
|
||||
let renders_path = renders::path();
|
||||
let fields = entries.iter().map(|entry| {
|
||||
let ident = &entry.ident;
|
||||
quote! { #ident: args.#ident, }
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl ::std::convert::From<RenderArgs> for #renders_path::RenderBase {
|
||||
fn from(args: RenderArgs) -> Self {
|
||||
Self {
|
||||
#( #fields )*
|
||||
..::std::default::Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::{
|
||||
ast::{Camera, FujiOption},
|
||||
common::{options, simulations},
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
id: String,
|
||||
ident: proc_macro2::Ident,
|
||||
type_path: TokenStream,
|
||||
}
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let simulation_options = collect_simulation_option_ids(cameras);
|
||||
let entries = build_entries(options);
|
||||
|
||||
let struct_def = generate_struct(&entries);
|
||||
let from_impl = generate_from_impl(&entries, &simulation_options);
|
||||
let prop_codes_const = generate_prop_codes_const(options, &simulation_options);
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#from_impl
|
||||
#prop_codes_const
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_simulation_option_ids(cameras: &BTreeMap<String, Camera>) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
for camera in cameras.values() {
|
||||
let Some(simulation) = camera
|
||||
.spec
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|f| f.simulation.as_ref())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for setting in &simulation.settings {
|
||||
out.insert(setting.r#ref.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn build_entries(options: &BTreeMap<String, FujiOption>) -> Vec<Entry> {
|
||||
options
|
||||
.values()
|
||||
.filter(|opt| !opt.codegen.skip_args)
|
||||
.map(|opt| {
|
||||
let ident = format_ident!("{}", opt.id);
|
||||
let type_ident = safe_upper_camel_case_ident(&opt.id);
|
||||
let options_path = options::path();
|
||||
let type_path = quote! { #options_path::#type_ident };
|
||||
Entry {
|
||||
id: opt.id.clone(),
|
||||
ident,
|
||||
type_path,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_struct(entries: &[Entry]) -> TokenStream {
|
||||
let fields = entries.iter().map(|entry| {
|
||||
let ident = &entry.ident;
|
||||
let ty = &entry.type_path;
|
||||
|
||||
let attrs = quote! { #[clap(long, allow_hyphen_values(true))] };
|
||||
|
||||
quote! {
|
||||
#attrs
|
||||
pub #ident: Option<#ty>,
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(::clap::Args, Debug, Default, Clone)]
|
||||
pub struct SimulationArgs {
|
||||
#( #fields )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_from_impl(entries: &[Entry], simulation_options: &BTreeSet<String>) -> TokenStream {
|
||||
let simulations_path = simulations::path();
|
||||
let mut fields: Vec<TokenStream> = Vec::new();
|
||||
let mut covered = 0usize;
|
||||
|
||||
for entry in entries {
|
||||
if !simulation_options.contains(&entry.id) {
|
||||
continue;
|
||||
}
|
||||
let ident = &entry.ident;
|
||||
fields.push(quote! { #ident: args.#ident, });
|
||||
covered += 1;
|
||||
}
|
||||
|
||||
let tail = if covered == simulation_options.len() {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { ..::std::default::Default::default() }
|
||||
};
|
||||
|
||||
quote! {
|
||||
impl ::std::convert::From<SimulationArgs> for #simulations_path::SimulationBase {
|
||||
fn from(args: SimulationArgs) -> Self {
|
||||
Self {
|
||||
#( #fields )*
|
||||
#tail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_prop_codes_const(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
simulation_options: &BTreeSet<String>,
|
||||
) -> TokenStream {
|
||||
let prop_codes = collect_simulation_prop_codes(options, simulation_options);
|
||||
let prop_code_lits = prop_codes
|
||||
.iter()
|
||||
.map(|c| proc_macro2::Literal::u16_suffixed(*c));
|
||||
|
||||
quote! {
|
||||
pub const SIMULATION_PROP_CODES: &[u16] = &[
|
||||
#( #prop_code_lits ),*
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_simulation_prop_codes(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
simulation_options: &BTreeSet<String>,
|
||||
) -> Vec<u16> {
|
||||
let mut codes: BTreeSet<u16> = BTreeSet::new();
|
||||
for id in simulation_options {
|
||||
let Some(option) = options.get(id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(code) = option.spec.prop_code() {
|
||||
codes.insert(code);
|
||||
}
|
||||
}
|
||||
codes.into_iter().collect()
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use proc_macro2::{Literal, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use crate::{
|
||||
ast::Camera,
|
||||
util::ident::{safe_upper_camel_case_ident, safe_uppercase_ident},
|
||||
};
|
||||
|
||||
pub fn generate(cameras: &BTreeMap<String, Camera>) -> anyhow::Result<TokenStream> {
|
||||
let mut sorted: Vec<&Camera> = cameras.values().collect();
|
||||
sorted.sort_by(|a, b| {
|
||||
(a.spec.generation.as_str(), a.id.as_str())
|
||||
.cmp(&(b.spec.generation.as_str(), b.id.as_str()))
|
||||
});
|
||||
|
||||
let mut defs = Vec::new();
|
||||
let mut supported_entries = Vec::new();
|
||||
|
||||
for camera in sorted {
|
||||
let struct_name = safe_upper_camel_case_ident(&camera.id);
|
||||
let const_name = safe_uppercase_ident(&format!("C_{}", camera.id));
|
||||
let name_str = &camera.spec.name;
|
||||
let vendor = Literal::u16_suffixed(camera.spec.usb.vendor_id);
|
||||
let product = Literal::u16_suffixed(camera.spec.usb.product_id);
|
||||
let chunk_size = Literal::usize_suffixed(camera.spec.usb.chunk_size.try_into()?);
|
||||
|
||||
let features = camera.spec.features.as_ref();
|
||||
let backup_override = features.is_some_and(|f| f.backup).then(|| {
|
||||
quote! {
|
||||
fn as_backup_manager(
|
||||
&self,
|
||||
) -> Option<&dyn crate::features::backup::CameraBackupManager<Context = Self::Context>> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
});
|
||||
let simulation_override = features.and_then(|f| f.simulation.as_ref()).map(|_| {
|
||||
quote! {
|
||||
fn as_simulation_parser(
|
||||
&self,
|
||||
) -> Option<&dyn crate::features::simulation::CameraSimulationParser> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn as_simulation_manager(
|
||||
&self,
|
||||
) -> Option<&dyn crate::features::simulation::CameraSimulationManager<Context = Self::Context>> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
});
|
||||
let render_override = features.and_then(|f| f.render.as_ref()).map(|_| {
|
||||
quote! {
|
||||
fn as_render_manager(
|
||||
&self,
|
||||
) -> Option<&dyn crate::features::render::CameraRenderManager<Context = Self::Context>> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
defs.push(quote! {
|
||||
pub struct #struct_name;
|
||||
|
||||
pub const #const_name: crate::SupportedCamera = crate::SupportedCamera {
|
||||
name: #name_str,
|
||||
vendor: #vendor,
|
||||
product: #product,
|
||||
camera_factory: || Box::new(#struct_name),
|
||||
};
|
||||
|
||||
impl crate::features::base::CameraBase for #struct_name {
|
||||
type Context = rusb::GlobalContext;
|
||||
|
||||
fn camera_definition(&self) -> &'static crate::SupportedCamera {
|
||||
&#const_name
|
||||
}
|
||||
|
||||
fn chunk_size(&self) -> usize {
|
||||
#chunk_size
|
||||
}
|
||||
|
||||
#backup_override
|
||||
#simulation_override
|
||||
#render_override
|
||||
}
|
||||
});
|
||||
supported_entries.push(quote! { #const_name });
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
//! Generated camera definitions and supported device registry. Do not edit.
|
||||
|
||||
#(#defs)*
|
||||
|
||||
pub const SUPPORTED: &[crate::SupportedCamera] = &[
|
||||
#(#supported_entries,)*
|
||||
];
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path() -> TokenStream {
|
||||
quote! { crate::generated::cameras }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod cameras;
|
||||
pub mod options;
|
||||
pub mod renders;
|
||||
pub mod simulations;
|
||||
@@ -0,0 +1,152 @@
|
||||
use anyhow::{Context, bail};
|
||||
use proc_macro2::{Ident, Literal, Span, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
pub fn resolve_enum_repr_signed(values: &[i32]) -> anyhow::Result<bool> {
|
||||
if values.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let min = values.iter().min().expect("values is non-empty");
|
||||
let max = values.iter().max().expect("values is non-empty");
|
||||
resolve_numeric_repr_signed(*min, *max)
|
||||
}
|
||||
|
||||
pub fn resolve_numeric_repr_signed(min: i32, max: i32) -> anyhow::Result<bool> {
|
||||
let negative = min < 0;
|
||||
let above_i16_max = max > i32::from(i16::MAX);
|
||||
let below_i16_min = min < i32::from(i16::MIN);
|
||||
let above_u16_max = max > i32::from(u16::MAX);
|
||||
|
||||
if below_i16_min || above_u16_max {
|
||||
bail!(
|
||||
"wire value range [{}, {}] fits neither i16 nor u16",
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
|
||||
if negative && above_i16_max {
|
||||
bail!(
|
||||
"wire values [{}, {}] mix negatives with values above i16::MAX",
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
|
||||
Ok(negative)
|
||||
}
|
||||
|
||||
pub fn resolve_repr_type(signed: bool) -> Ident {
|
||||
if signed {
|
||||
Ident::new("i16", Span::call_site())
|
||||
} else {
|
||||
Ident::new("u16", Span::call_site())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_repr_type_32(signed: bool) -> Ident {
|
||||
if signed {
|
||||
Ident::new("i32", Span::call_site())
|
||||
} else {
|
||||
Ident::new("u32", Span::call_site())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wire_literal(value: i32, signed: bool) -> anyhow::Result<Literal> {
|
||||
if signed {
|
||||
let val: i16 = value
|
||||
.try_into()
|
||||
.with_context(|| format!("wire value {value} does not fit in i16"))?;
|
||||
Ok(Literal::i16_suffixed(val))
|
||||
} else {
|
||||
let val: u16 = value
|
||||
.try_into()
|
||||
.with_context(|| format!("wire value {value} does not fit in u16"))?;
|
||||
Ok(Literal::u16_suffixed(val))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_try_from_wire_impl(
|
||||
type_name: &Ident,
|
||||
signed: bool,
|
||||
repr_type: &Ident,
|
||||
items: &[(Ident, Vec<i32>)],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let arms = items
|
||||
.iter()
|
||||
.map(|(variant, wires)| {
|
||||
let lits = wires
|
||||
.iter()
|
||||
.map(|w| wire_literal(*w, signed))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(quote! { #(#lits)|* => Ok(Self::#variant), })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #type_name {
|
||||
fn try_from_wire(value: #repr_type) -> ::std::io::Result<Self> {
|
||||
match value {
|
||||
#(#arms)*
|
||||
_ => Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid {} discriminant {}",
|
||||
stringify!(#type_name), value,
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::resolve_enum_repr_signed;
|
||||
|
||||
#[test]
|
||||
fn test_empty_values_defaults_to_unsigned() {
|
||||
let values: [i32; 0] = [];
|
||||
let result = resolve_enum_repr_signed(&values);
|
||||
assert!(!result.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_in_i16_positive_range_defaults_to_unsigned() {
|
||||
let values = [0, 100, i16::MAX as i32];
|
||||
assert!(!resolve_enum_repr_signed(&values).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_any_negative_forces_signed() {
|
||||
let values = [-3000, 0, 100];
|
||||
assert!(resolve_enum_repr_signed(&values).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_any_above_i16_max_forces_unsigned() {
|
||||
let values = [0, 40000];
|
||||
assert!(!resolve_enum_repr_signed(&values).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_negatives_and_large_positives_fails() {
|
||||
let values = [-1, 40000];
|
||||
let result = resolve_enum_repr_signed(&values);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_absolute_bounds_fails() {
|
||||
assert!(resolve_enum_repr_signed(&[-32769]).is_err());
|
||||
assert!(resolve_enum_repr_signed(&[65536]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boundaries() {
|
||||
assert!(resolve_enum_repr_signed(&[i16::MIN as i32]).unwrap());
|
||||
assert!(!resolve_enum_repr_signed(&[u16::MAX as i32]).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use super::common::{
|
||||
generate_try_from_wire_impl, resolve_enum_repr_signed, resolve_repr_type, resolve_repr_type_32,
|
||||
wire_literal,
|
||||
};
|
||||
use crate::{
|
||||
ast::{EnumEncoding, EnumRules, EnumVariant, LookupSpec, LookupValue},
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
struct Resolved<'a> {
|
||||
variant: &'a EnumVariant,
|
||||
canonical: i32,
|
||||
alternates: Vec<i32>,
|
||||
}
|
||||
|
||||
impl<'a> Resolved<'a> {
|
||||
fn from_variant(variant: &'a EnumVariant, spec: &LookupSpec) -> anyhow::Result<Self> {
|
||||
let lookup_value = spec
|
||||
.values
|
||||
.get(&variant.id)
|
||||
.with_context(|| format!("missing lookup entry for variant `{}`", variant.id))?;
|
||||
|
||||
let (canonical, alternates) = match lookup_value {
|
||||
LookupValue::Single(n) => (*n, Vec::new()),
|
||||
LookupValue::Multi(list) => {
|
||||
let (&canonical, rest) = list.split_first().with_context(|| {
|
||||
format!("empty multi-value lookup for variant `{}`", variant.id)
|
||||
})?;
|
||||
|
||||
(canonical, rest.to_vec())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
variant,
|
||||
canonical,
|
||||
alternates,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
id: &str,
|
||||
rules: &EnumRules,
|
||||
encoding: &EnumEncoding,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let EnumEncoding::Lookup { spec, prop_code } = encoding;
|
||||
|
||||
let resolved = rules
|
||||
.variants
|
||||
.iter()
|
||||
.map(|v| Resolved::from_variant(v, spec))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
let wire_values: Vec<_> = resolved
|
||||
.iter()
|
||||
.flat_map(|r| std::iter::once(&r.canonical).chain(&r.alternates))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let signed = resolve_enum_repr_signed(&wire_values)
|
||||
.with_context(|| format!("determining representation type for enum option `{id}`"))?;
|
||||
let repr_type = resolve_repr_type(signed);
|
||||
let repr_type_32 = resolve_repr_type_32(signed);
|
||||
|
||||
let enum_def = generate_enum_def(
|
||||
&safe_upper_camel_case_ident(id),
|
||||
&repr_type,
|
||||
signed,
|
||||
&resolved,
|
||||
)
|
||||
.with_context(|| format!("generating enum definition for enum option `{id}`"))?;
|
||||
let try_from_wire_impl = generate_try_from_wire_impl(
|
||||
&safe_upper_camel_case_ident(id),
|
||||
signed,
|
||||
&repr_type,
|
||||
&wire_items(&resolved),
|
||||
)
|
||||
.with_context(|| format!("generating try_from_wire impl for enum option `{id}`"))?;
|
||||
let display_impl = generate_display_impl(&safe_upper_camel_case_ident(id), &resolved)
|
||||
.with_context(|| format!("generating Display impl for enum option `{id}`"))?;
|
||||
let from_str_impl = generate_from_str_impl(&safe_upper_camel_case_ident(id), &resolved)
|
||||
.with_context(|| format!("generating FromStr impl for enum option `{id}`"))?;
|
||||
|
||||
let (ptp_serde_impl, simulation_setting_impl) = if let Some(prop_code) = prop_code {
|
||||
let serde = generate_ptp_serde_impl(&safe_upper_camel_case_ident(id), &repr_type)
|
||||
.with_context(|| {
|
||||
format!("generating PtpSerialize/PtpDeserialize impls for enum option `{id}`")
|
||||
})?;
|
||||
let setting =
|
||||
generate_simulation_setting_impl(&safe_upper_camel_case_ident(id), *prop_code)
|
||||
.with_context(|| {
|
||||
format!("generating SimulationSetting impl for enum option `{id}`")
|
||||
})?;
|
||||
(serde, setting)
|
||||
} else {
|
||||
(quote! {}, quote! {})
|
||||
};
|
||||
|
||||
let conversion_profile_impl = generate_conversion_profile_impl(
|
||||
&safe_upper_camel_case_ident(id),
|
||||
&repr_type,
|
||||
&repr_type_32,
|
||||
)
|
||||
.with_context(|| format!("generating ConversionProfileField impl for enum option `{id}`"))?;
|
||||
|
||||
Ok(quote! {
|
||||
#enum_def
|
||||
#try_from_wire_impl
|
||||
#display_impl
|
||||
#from_str_impl
|
||||
#ptp_serde_impl
|
||||
#simulation_setting_impl
|
||||
#conversion_profile_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn wire_items(resolved: &[Resolved<'_>]) -> Vec<(Ident, Vec<i32>)> {
|
||||
resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let mut wires = Vec::with_capacity(1 + r.alternates.len());
|
||||
wires.push(r.canonical);
|
||||
wires.extend(r.alternates.iter().copied());
|
||||
(safe_upper_camel_case_ident(&r.variant.id), wires)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_enum_def(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
signed: bool,
|
||||
resolved: &[Resolved<'_>],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let defs = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = safe_upper_camel_case_ident(&r.variant.id);
|
||||
let canonical = wire_literal(r.canonical, signed)?;
|
||||
Ok(quote! { #v = #canonical, })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#[repr(#repr_type)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum_macros::EnumIter,
|
||||
serde_with::SerializeDisplay,
|
||||
serde_with::DeserializeFromStr,
|
||||
)]
|
||||
pub enum #type_name {
|
||||
#(#defs)*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_display_impl(
|
||||
type_name: &Ident,
|
||||
resolved: &[Resolved<'_>],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let arms = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = safe_upper_camel_case_ident(&r.variant.id);
|
||||
let display = &r.variant.name;
|
||||
quote! { Self::#v => write!(f, #display), }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
match self {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_str_impl(
|
||||
type_name: &Ident,
|
||||
resolved: &[Resolved<'_>],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let arms = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = safe_upper_camel_case_ident(&r.variant.id);
|
||||
let aliases = &r.variant.aliases;
|
||||
quote! {
|
||||
#(#aliases)|* => return Ok(Self::#v),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::anyhow::Error;
|
||||
fn from_str(s: &str) -> ::anyhow::Result<Self> {
|
||||
match crate::input::CleanAlphanumeric::clean(&s).as_str() {
|
||||
#(#arms)*
|
||||
_ => {}
|
||||
}
|
||||
if let Some(best) = <Self as crate::input::Choices>::closest(s) {
|
||||
::anyhow::bail!(
|
||||
"Unknown {} '{s}'. Did you mean '{best}'?",
|
||||
stringify!(#type_name),
|
||||
);
|
||||
}
|
||||
::anyhow::bail!("Unknown {} '{s}'", stringify!(#type_name));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_ptp_serde_impl(type_name: &Ident, repr_type: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::ptp_cursor::PtpSerialize for #type_name {
|
||||
fn try_into_ptp(&self) -> ::std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
<Self as ::ptp_cursor::PtpSerialize>::try_write_ptp(self, &mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> ::std::io::Result<()> {
|
||||
let raw: #repr_type = *self as #repr_type;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&raw, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::ptp_cursor::PtpDeserialize for #type_name {
|
||||
fn try_from_ptp(buf: &[u8]) -> ::std::io::Result<Self> {
|
||||
let mut cur = ::std::io::Cursor::new(buf);
|
||||
let val = <Self as ::ptp_cursor::PtpDeserialize>::try_read_ptp(&mut cur)?;
|
||||
::ptp_cursor::Read::expect_end(&mut cur)?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn try_read_ptp<R: ::ptp_cursor::Read>(cur: &mut R) -> ::std::io::Result<Self> {
|
||||
let raw = <#repr_type as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
Self::try_from_wire(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_simulation_setting_impl(
|
||||
type_name: &Ident,
|
||||
prop_code: u16,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::SimulationSetting for #type_name {
|
||||
fn prop_code() -> u16 { #prop_code }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_conversion_profile_impl(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
repr_type_32: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::ConversionProfileField for #type_name {
|
||||
fn try_write_conversion_profile_field_ptp(
|
||||
&self, buf: &mut Vec<u8>,
|
||||
) -> ::std::io::Result<()> {
|
||||
let raw: #repr_type = *self as #repr_type;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&#repr_type_32::from(raw), buf)
|
||||
}
|
||||
|
||||
fn try_read_conversion_profile_field_ptp<R: ::ptp_cursor::Read>(
|
||||
cur: &mut R,
|
||||
) -> ::std::io::Result<Self> {
|
||||
let extended = <#repr_type_32 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
let raw: #repr_type = extended.try_into().map_err(|_| {
|
||||
::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} value {} doesn't fit in {}",
|
||||
stringify!(#type_name),
|
||||
extended,
|
||||
stringify!(#repr_type),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Self::try_from_wire(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use super::super::common::{
|
||||
generate_try_from_wire_impl, resolve_enum_repr_signed, resolve_repr_type, resolve_repr_type_32,
|
||||
wire_literal,
|
||||
};
|
||||
use crate::{
|
||||
ast::{LookupSpec, LookupValue},
|
||||
util::ident::{numeric_variant_ident, safe_upper_camel_case_ident},
|
||||
};
|
||||
|
||||
struct Resolved {
|
||||
ident: Ident,
|
||||
logical: f32,
|
||||
canonical: i32,
|
||||
alternates: Vec<i32>,
|
||||
}
|
||||
|
||||
impl Resolved {
|
||||
fn from_spec_entry(key: &str, value: &LookupValue) -> anyhow::Result<Self> {
|
||||
let logical: f32 = key
|
||||
.parse()
|
||||
.with_context(|| format!("float-lookup key `{key}` is not an f32"))?;
|
||||
|
||||
let (canonical, alternates) = match value {
|
||||
LookupValue::Single(n) => (*n, Vec::new()),
|
||||
LookupValue::Multi(list) => {
|
||||
let (&canonical, rest) = list
|
||||
.split_first()
|
||||
.with_context(|| format!("empty multi-value lookup for key `{key}`"))?;
|
||||
(canonical, rest.to_vec())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
ident: numeric_variant_ident(key),
|
||||
logical,
|
||||
canonical,
|
||||
alternates,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
id: &str,
|
||||
prop_code: Option<u16>,
|
||||
spec: &LookupSpec,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let mut resolved: Vec<_> = spec
|
||||
.values
|
||||
.iter()
|
||||
.map(|(key, value)| Resolved::from_spec_entry(key, value))
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.with_context(|| format!("resolving lookup entries for float option `{id}`"))?;
|
||||
resolved.sort_by(|a, b| {
|
||||
a.logical
|
||||
.partial_cmp(&b.logical)
|
||||
.unwrap_or(::std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
let wire_values: Vec<_> = resolved
|
||||
.iter()
|
||||
.flat_map(|r| std::iter::once(&r.canonical).chain(&r.alternates))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let signed = resolve_enum_repr_signed(&wire_values)
|
||||
.with_context(|| format!("determining representation type for float option `{id}`"))?;
|
||||
let repr_type = resolve_repr_type(signed);
|
||||
let repr_type_32 = resolve_repr_type_32(signed);
|
||||
|
||||
let type_name = safe_upper_camel_case_ident(id);
|
||||
|
||||
let enum_def = generate_enum_def(&type_name, &repr_type, signed, &resolved)
|
||||
.with_context(|| format!("generating enum definition for float option `{id}`"))?;
|
||||
let inherent_impl = generate_inherent_impl(&type_name, &resolved)
|
||||
.with_context(|| format!("generating inherent impl for float option `{id}`"))?;
|
||||
let try_from_wire_impl = generate_try_from_wire_impl(
|
||||
&safe_upper_camel_case_ident(id),
|
||||
signed,
|
||||
&repr_type,
|
||||
&wire_items(&resolved),
|
||||
)
|
||||
.with_context(|| format!("generating try_from_wire impl for float option `{id}`"))?;
|
||||
let try_from_logical_impl = generate_try_from_logical_impl(&type_name)
|
||||
.with_context(|| format!("generating TryFrom<f32> impl for float option `{id}`"))?;
|
||||
let to_logical_impl = generate_to_logical_impl(&type_name, &resolved).with_context(|| {
|
||||
format!("generating From<{type_name}> for f32 impl for float option `{id}`")
|
||||
})?;
|
||||
let display_impl = generate_display_impl(&type_name)
|
||||
.with_context(|| format!("generating Display impl for float option `{id}`"))?;
|
||||
let from_str_impl = generate_from_str_impl(&type_name)
|
||||
.with_context(|| format!("generating FromStr impl for float option `{id}`"))?;
|
||||
let serde_impls = generate_serde_impls(&type_name)
|
||||
.with_context(|| format!("generating Serde impls for float option `{id}`"))?;
|
||||
let (ptp_serde_impl, simulation_setting_impl) = if let Some(code) = prop_code {
|
||||
let serde = generate_ptp_serde_impl(&type_name, &repr_type).with_context(|| {
|
||||
format!("generating PtpSerialize/PtpDeserialize impls for float option `{id}`")
|
||||
})?;
|
||||
let setting = generate_simulation_setting_impl(&type_name, code).with_context(|| {
|
||||
format!("generating SimulationSetting impl for float option `{id}`")
|
||||
})?;
|
||||
(serde, setting)
|
||||
} else {
|
||||
(quote! {}, quote! {})
|
||||
};
|
||||
let conversion_profile_impl =
|
||||
generate_conversion_profile_impl(&type_name, &repr_type, &repr_type_32).with_context(
|
||||
|| format!("generating ConversionProfileField impl for float option `{id}`"),
|
||||
)?;
|
||||
|
||||
Ok(quote! {
|
||||
#enum_def
|
||||
#inherent_impl
|
||||
#try_from_wire_impl
|
||||
#try_from_logical_impl
|
||||
#to_logical_impl
|
||||
#display_impl
|
||||
#from_str_impl
|
||||
#serde_impls
|
||||
#ptp_serde_impl
|
||||
#simulation_setting_impl
|
||||
#conversion_profile_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn wire_items(resolved: &[Resolved]) -> Vec<(Ident, Vec<i32>)> {
|
||||
resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let mut wires = Vec::with_capacity(1 + r.alternates.len());
|
||||
wires.push(r.canonical);
|
||||
wires.extend(r.alternates.iter().copied());
|
||||
(r.ident.clone(), wires)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_enum_def(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
signed: bool,
|
||||
resolved: &[Resolved],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let defs = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let canonical = wire_literal(r.canonical, signed)?;
|
||||
Ok(quote! { #v = #canonical, })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#[repr(#repr_type)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum_macros::EnumIter,
|
||||
)]
|
||||
pub enum #type_name {
|
||||
#(#defs)*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(type_name: &Ident, resolved: &[Resolved]) -> anyhow::Result<TokenStream> {
|
||||
let values_const: Vec<TokenStream> = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let logical = r.logical;
|
||||
quote! { (#logical, Self::#v), }
|
||||
})
|
||||
.collect();
|
||||
|
||||
let logical_min = resolved.first().map(|r| r.logical).unwrap_or(0.0);
|
||||
let logical_max = resolved.last().map(|r| r.logical).unwrap_or(0.0);
|
||||
|
||||
Ok(quote! {
|
||||
impl #type_name {
|
||||
const VALUES: &'static [(f32, Self)] = &[
|
||||
#(#values_const)*
|
||||
];
|
||||
|
||||
pub const LOGICAL_MIN: f32 = #logical_min;
|
||||
pub const LOGICAL_MAX: f32 = #logical_max;
|
||||
|
||||
pub fn from_nearest_f32(value: f32) -> Self {
|
||||
Self::VALUES
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
let da = (a.0 - value).abs();
|
||||
let db = (b.0 - value).abs();
|
||||
da.partial_cmp(&db).unwrap_or(::std::cmp::Ordering::Equal)
|
||||
})
|
||||
.map(|(_, v)| *v)
|
||||
.unwrap_or(Self::VALUES[0].1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_from_logical_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::convert::TryFrom<f32> for #type_name {
|
||||
type Error = ::anyhow::Error;
|
||||
fn try_from(value: f32) -> ::anyhow::Result<Self> {
|
||||
Self::VALUES
|
||||
.iter()
|
||||
.find(|(v, _)| (*v - value).abs() < f32::EPSILON)
|
||||
.map(|(_, variant)| *variant)
|
||||
.ok_or_else(|| ::anyhow::anyhow!(
|
||||
"Value {} is not a valid {}",
|
||||
value, stringify!(#type_name),
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_to_logical_impl(
|
||||
type_name: &Ident,
|
||||
resolved: &[Resolved],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let arms: Vec<_> = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let logical = r.logical;
|
||||
quote! { #type_name::#v => #logical, }
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::convert::From<#type_name> for f32 {
|
||||
fn from(value: #type_name) -> f32 {
|
||||
match value {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_display_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
let n = f32::from(*self);
|
||||
if n == 0.0 { write!(f, "0") } else { write!(f, "{n:+}") }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_str_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::anyhow::Error;
|
||||
fn from_str(s: &str) -> ::anyhow::Result<Self> {
|
||||
let value = crate::input::CleanAlphanumeric::clean(&s)
|
||||
.parse::<f32>()
|
||||
.map_err(|e| ::anyhow::anyhow!("Invalid numeric value '{}': {}", s, e))?;
|
||||
if !(Self::LOGICAL_MIN..=Self::LOGICAL_MAX).contains(&value) {
|
||||
::anyhow::bail!(
|
||||
"{} value {} is out of range [{}, {}]",
|
||||
stringify!(#type_name), value, Self::LOGICAL_MIN, Self::LOGICAL_MAX,
|
||||
);
|
||||
}
|
||||
Ok(Self::from_nearest_f32(value))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_serde_impls(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::serde::Serialize for #type_name {
|
||||
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_f32(f32::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> ::serde::Deserialize<'de> for #type_name {
|
||||
fn deserialize<D: ::serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Self, D::Error> {
|
||||
let logical = f32::deserialize(deserializer)?;
|
||||
<Self as ::std::convert::TryFrom<f32>>::try_from(logical)
|
||||
.map_err(::serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_ptp_serde_impl(type_name: &Ident, repr_type: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::ptp_cursor::PtpSerialize for #type_name {
|
||||
fn try_into_ptp(&self) -> ::std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
<Self as ::ptp_cursor::PtpSerialize>::try_write_ptp(self, &mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> ::std::io::Result<()> {
|
||||
let raw: #repr_type = *self as #repr_type;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&raw, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::ptp_cursor::PtpDeserialize for #type_name {
|
||||
fn try_from_ptp(buf: &[u8]) -> ::std::io::Result<Self> {
|
||||
let mut cur = ::std::io::Cursor::new(buf);
|
||||
let val = <Self as ::ptp_cursor::PtpDeserialize>::try_read_ptp(&mut cur)?;
|
||||
::ptp_cursor::Read::expect_end(&mut cur)?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn try_read_ptp<R: ::ptp_cursor::Read>(cur: &mut R) -> ::std::io::Result<Self> {
|
||||
let raw = <#repr_type as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
Self::try_from_wire(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_simulation_setting_impl(
|
||||
type_name: &Ident,
|
||||
prop_code: u16,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::SimulationSetting for #type_name {
|
||||
fn prop_code() -> u16 { #prop_code }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_conversion_profile_impl(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
repr_type_32: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::ConversionProfileField for #type_name {
|
||||
fn try_write_conversion_profile_field_ptp(
|
||||
&self, buf: &mut Vec<u8>,
|
||||
) -> ::std::io::Result<()> {
|
||||
let raw: #repr_type = *self as #repr_type;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&#repr_type_32::from(raw), buf)
|
||||
}
|
||||
|
||||
fn try_read_conversion_profile_field_ptp<R: ::ptp_cursor::Read>(
|
||||
cur: &mut R,
|
||||
) -> ::std::io::Result<Self> {
|
||||
let extended = <#repr_type_32 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
let raw: #repr_type = extended.try_into().map_err(|_| {
|
||||
::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} value {} doesn't fit in {}",
|
||||
stringify!(#type_name),
|
||||
extended,
|
||||
stringify!(#repr_type),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Self::try_from_wire(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod lookup;
|
||||
pub mod scaled;
|
||||
@@ -0,0 +1,286 @@
|
||||
use anyhow::{Context, bail};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use super::super::common::{resolve_numeric_repr_signed, resolve_repr_type, resolve_repr_type_32};
|
||||
use crate::{
|
||||
ast::{NumericEncoding, NumericRules},
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
struct Bounds {
|
||||
min: f32,
|
||||
max: f32,
|
||||
step: f32,
|
||||
scale: i32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn resolve(
|
||||
id: &str,
|
||||
rules: Option<&NumericRules<f32>>,
|
||||
encoding: &NumericEncoding,
|
||||
) -> anyhow::Result<Self> {
|
||||
let min = rules.and_then(|r| r.min).unwrap_or(f32::MIN);
|
||||
let max = rules.and_then(|r| r.max).unwrap_or(f32::MAX);
|
||||
let step = rules.and_then(|r| r.step).unwrap_or(1.0);
|
||||
let scale = match encoding {
|
||||
NumericEncoding::Raw { .. } => 1,
|
||||
NumericEncoding::Scale { spec, .. } => spec.scale,
|
||||
NumericEncoding::Lookup { .. } => {
|
||||
bail!("float-lookup option `{id}` should use the lookup generator");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
id: &str,
|
||||
prop_code: Option<u16>,
|
||||
rules: Option<&NumericRules<f32>>,
|
||||
encoding: &NumericEncoding,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let bounds = Bounds::resolve(id, rules, encoding)
|
||||
.with_context(|| format!("resolving bounds for float option `{id}`"))?;
|
||||
|
||||
let raw_min = (bounds.min * bounds.scale as f32).round() as i32;
|
||||
let raw_max = (bounds.max * bounds.scale as f32).round() as i32;
|
||||
let signed = resolve_numeric_repr_signed(raw_min, raw_max)
|
||||
.with_context(|| format!("determining representation type for float option `{id}`"))?;
|
||||
|
||||
let repr_type = resolve_repr_type(signed);
|
||||
let repr_type_32 = resolve_repr_type_32(signed);
|
||||
|
||||
let type_name = safe_upper_camel_case_ident(id);
|
||||
|
||||
let struct_def = generate_struct_def(&type_name)
|
||||
.with_context(|| format!("generating struct definition for float option `{id}`"))?;
|
||||
let inherent_impl = generate_inherent_impl(&type_name, signed, &bounds)
|
||||
.with_context(|| format!("generating inherent impl for float option `{id}`"))?;
|
||||
let try_from_impl = generate_try_from_impl(&type_name)
|
||||
.with_context(|| format!("generating TryFrom<f32> impl for float option `{id}`"))?;
|
||||
let from_impl = generate_from_impl(&type_name).with_context(|| {
|
||||
format!("generating From<{type_name}> for f32 impl for float option `{id}`")
|
||||
})?;
|
||||
let display_impl = generate_display_impl(&type_name)
|
||||
.with_context(|| format!("generating Display impl for float option `{id}`"))?;
|
||||
let from_str_impl = generate_from_str_impl(&type_name)
|
||||
.with_context(|| format!("generating FromStr impl for float option `{id}`"))?;
|
||||
let serde_impls = generate_serde_impls(&type_name)
|
||||
.with_context(|| format!("generating Serde impls for float option `{id}`"))?;
|
||||
let simulation_setting_impl = if let Some(code) = prop_code {
|
||||
generate_simulation_setting_impl(&type_name, code)
|
||||
.with_context(|| format!("generating SimulationSetting impl for float option `{id}`"))?
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let conversion_profile_impl =
|
||||
generate_conversion_profile_impl(&type_name, &repr_type, &repr_type_32).with_context(
|
||||
|| format!("generating ConversionProfileField impl for float option `{id}`"),
|
||||
)?;
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#inherent_impl
|
||||
#try_from_impl
|
||||
#from_impl
|
||||
#display_impl
|
||||
#from_str_impl
|
||||
#serde_impls
|
||||
#simulation_setting_impl
|
||||
#conversion_profile_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_struct_def(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
ptp_macro::PtpSerialize,
|
||||
ptp_macro::PtpDeserialize,
|
||||
)]
|
||||
pub struct #type_name(i16);
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(
|
||||
type_name: &Ident,
|
||||
signed: bool,
|
||||
bounds: &Bounds,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let Bounds {
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
scale,
|
||||
} = *bounds;
|
||||
|
||||
let logical = quote! {
|
||||
pub const MIN: f32 = #min;
|
||||
pub const MAX: f32 = #max;
|
||||
pub const STEP: f32 = #step;
|
||||
pub const SCALE: i32 = #scale;
|
||||
};
|
||||
|
||||
let raw = if signed {
|
||||
let raw_min: i16 = ((min * scale as f32) as i32).try_into()?;
|
||||
let raw_max: i16 = ((max * scale as f32) as i32).try_into()?;
|
||||
let raw_step: i16 = ((step * scale as f32) as i32).try_into()?;
|
||||
|
||||
quote! {
|
||||
pub const RAW_MIN: i16 = #raw_min;
|
||||
pub const RAW_MAX: i16 = #raw_max;
|
||||
pub const RAW_STEP: i16 = #raw_step;
|
||||
}
|
||||
} else {
|
||||
let raw_min: u16 = ((min * scale as f32) as i32).try_into()?;
|
||||
let raw_max: u16 = ((max * scale as f32) as i32).try_into()?;
|
||||
let raw_step: u16 = ((step * scale as f32) as i32).try_into()?;
|
||||
|
||||
quote! {
|
||||
pub const RAW_MIN: u16 = #raw_min;
|
||||
pub const RAW_MAX: u16 = #raw_max;
|
||||
pub const RAW_STEP: u16 = #raw_step;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #type_name {
|
||||
#logical
|
||||
#raw
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_from_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::convert::TryFrom<f32> for #type_name {
|
||||
type Error = ::anyhow::Error;
|
||||
fn try_from(value: f32) -> ::anyhow::Result<Self> {
|
||||
if !(Self::MIN..=Self::MAX).contains(&value) {
|
||||
::anyhow::bail!(
|
||||
"{} value {} is out of range [{}, {}]",
|
||||
stringify!(#type_name), value, Self::MIN, Self::MAX,
|
||||
);
|
||||
}
|
||||
if (value - Self::MIN) % Self::STEP != 0.0 {
|
||||
::anyhow::bail!(
|
||||
"{} value {} is not aligned to step {}",
|
||||
stringify!(#type_name), value, Self::STEP,
|
||||
);
|
||||
}
|
||||
let raw: i32 = (value * Self::SCALE as f32).round() as i32;
|
||||
let raw = raw.try_into()?;
|
||||
Ok(Self(raw))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::convert::From<#type_name> for f32 {
|
||||
fn from(value: #type_name) -> f32 {
|
||||
f32::from(value.0) / #type_name::SCALE as f32
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_display_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
write!(f, "{}", f32::from(*self))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_str_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::anyhow::Error;
|
||||
fn from_str(s: &str) -> ::anyhow::Result<Self> {
|
||||
let logical = crate::input::CleanAlphanumeric::clean(&s)
|
||||
.parse::<f32>()
|
||||
.map_err(|e| ::anyhow::anyhow!("Invalid numeric value '{}': {}", s, e))?;
|
||||
Self::try_from(logical)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_serde_impls(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::serde::Serialize for #type_name {
|
||||
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_f32(f32::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> ::serde::Deserialize<'de> for #type_name {
|
||||
fn deserialize<D: ::serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Self, D::Error> {
|
||||
let logical = f32::deserialize(deserializer)?;
|
||||
Self::try_from(logical).map_err(::serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_simulation_setting_impl(
|
||||
type_name: &Ident,
|
||||
prop_code: u16,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::SimulationSetting for #type_name {
|
||||
fn prop_code() -> u16 { #prop_code }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_conversion_profile_impl(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
repr_type_32: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::ConversionProfileField for #type_name {
|
||||
fn try_write_conversion_profile_field_ptp(
|
||||
&self, buf: &mut Vec<u8>,
|
||||
) -> ::std::io::Result<()> {
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&#repr_type_32::from(self.0), buf)
|
||||
}
|
||||
|
||||
fn try_read_conversion_profile_field_ptp<R: ::ptp_cursor::Read>(
|
||||
cur: &mut R,
|
||||
) -> ::std::io::Result<Self> {
|
||||
let extended = <#repr_type_32 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
let raw: #repr_type = extended.try_into().map_err(|_| {
|
||||
::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} value {} doesn't fit in {}",
|
||||
stringify!(#type_name),
|
||||
extended,
|
||||
stringify!(#repr_type),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok(Self(raw))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use super::super::common::{
|
||||
generate_try_from_wire_impl, resolve_enum_repr_signed, resolve_repr_type, resolve_repr_type_32,
|
||||
wire_literal,
|
||||
};
|
||||
use crate::{
|
||||
ast::{LookupSpec, LookupValue},
|
||||
util::ident::{numeric_variant_ident, safe_upper_camel_case_ident},
|
||||
};
|
||||
|
||||
struct Resolved {
|
||||
ident: Ident,
|
||||
logical: i32,
|
||||
canonical: i32,
|
||||
alternates: Vec<i32>,
|
||||
}
|
||||
|
||||
impl Resolved {
|
||||
fn from_spec_entry(key: &str, value: &LookupValue) -> anyhow::Result<Self> {
|
||||
let logical = key
|
||||
.parse()
|
||||
.with_context(|| format!("integer-lookup key `{key}` is not an integer"))?;
|
||||
|
||||
let (canonical, alternates) = match value {
|
||||
LookupValue::Single(n) => (*n, Vec::new()),
|
||||
LookupValue::Multi(list) => {
|
||||
let (&canonical, rest) = list
|
||||
.split_first()
|
||||
.with_context(|| format!("empty multi-value lookup for key `{key}`"))?;
|
||||
(canonical, rest.to_vec())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
ident: numeric_variant_ident(key),
|
||||
logical,
|
||||
canonical,
|
||||
alternates,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
id: &str,
|
||||
prop_code: Option<u16>,
|
||||
spec: &LookupSpec,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let mut resolved: Vec<_> = spec
|
||||
.values
|
||||
.iter()
|
||||
.map(|(key, value)| Resolved::from_spec_entry(key, value))
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.with_context(|| format!("resolving lookup entries for integer option `{id}`"))?;
|
||||
resolved.sort_by_key(|r| r.logical);
|
||||
|
||||
let wire_values: Vec<_> = resolved
|
||||
.iter()
|
||||
.flat_map(|r| std::iter::once(&r.canonical).chain(&r.alternates))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let signed = resolve_enum_repr_signed(&wire_values)
|
||||
.with_context(|| format!("determining representation type for integer option `{id}`"))?;
|
||||
let repr_type = resolve_repr_type(signed);
|
||||
let repr_type_32 = resolve_repr_type_32(signed);
|
||||
|
||||
let type_name = safe_upper_camel_case_ident(id);
|
||||
|
||||
let enum_def = generate_enum_def(&type_name, &repr_type, signed, &resolved)
|
||||
.with_context(|| format!("generating enum definition for integer option `{id}`"))?;
|
||||
let inherent_impl = generate_inherent_impl(&type_name, &resolved)
|
||||
.with_context(|| format!("generating inherent impl for integer option `{id}`"))?;
|
||||
let try_from_wire_impl = generate_try_from_wire_impl(
|
||||
&safe_upper_camel_case_ident(id),
|
||||
signed,
|
||||
&repr_type,
|
||||
&wire_items(&resolved),
|
||||
)
|
||||
.with_context(|| format!("generating try_from_wire impl for integer option `{id}`"))?;
|
||||
let try_from_logical_impl = generate_try_from_logical_impl(&type_name, &resolved)
|
||||
.with_context(|| format!("generating TryFrom<i32> impl for integer option `{id}`"))?;
|
||||
let to_logical_impl = generate_to_logical_impl(&type_name, &resolved).with_context(|| {
|
||||
format!("generating From<{type_name}> for i32 impl for integer option `{id}`")
|
||||
})?;
|
||||
let display_impl = generate_display_impl(&type_name)
|
||||
.with_context(|| format!("generating Display impl for integer option `{id}`"))?;
|
||||
let from_str_impl = generate_from_str_impl(&type_name)
|
||||
.with_context(|| format!("generating FromStr impl for integer option `{id}`"))?;
|
||||
let serde_impls = generate_serde_impls(&type_name)
|
||||
.with_context(|| format!("generating Serde impls for integer option `{id}`"))?;
|
||||
let (ptp_serde_impl, simulation_setting_impl) = if let Some(code) = prop_code {
|
||||
let serde = generate_ptp_serde_impl(&type_name, &repr_type).with_context(|| {
|
||||
format!("generating PtpSerialize/PtpDeserialize impls for integer option `{id}`")
|
||||
})?;
|
||||
let setting = generate_simulation_setting_impl(&type_name, code).with_context(|| {
|
||||
format!("generating SimulationSetting impl for integer option `{id}`")
|
||||
})?;
|
||||
(serde, setting)
|
||||
} else {
|
||||
(quote! {}, quote! {})
|
||||
};
|
||||
let conversion_profile_impl =
|
||||
generate_conversion_profile_impl(&type_name, &repr_type, &repr_type_32).with_context(
|
||||
|| format!("generating ConversionProfileField impl for integer option `{id}`"),
|
||||
)?;
|
||||
|
||||
Ok(quote! {
|
||||
#enum_def
|
||||
#inherent_impl
|
||||
#try_from_wire_impl
|
||||
#try_from_logical_impl
|
||||
#to_logical_impl
|
||||
#display_impl
|
||||
#from_str_impl
|
||||
#serde_impls
|
||||
#ptp_serde_impl
|
||||
#simulation_setting_impl
|
||||
#conversion_profile_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn wire_items(resolved: &[Resolved]) -> Vec<(Ident, Vec<i32>)> {
|
||||
resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let mut wires = Vec::with_capacity(1 + r.alternates.len());
|
||||
wires.push(r.canonical);
|
||||
wires.extend(r.alternates.iter().copied());
|
||||
(r.ident.clone(), wires)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_enum_def(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
signed: bool,
|
||||
resolved: &[Resolved],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let defs = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let canonical = wire_literal(r.canonical, signed)?;
|
||||
Ok(quote! { #v = #canonical, })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#[repr(#repr_type)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum_macros::EnumIter,
|
||||
)]
|
||||
pub enum #type_name {
|
||||
#(#defs)*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(type_name: &Ident, resolved: &[Resolved]) -> anyhow::Result<TokenStream> {
|
||||
let values_const: Vec<_> = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let logical = r.logical as f32;
|
||||
quote! { (#logical, Self::#v), }
|
||||
})
|
||||
.collect();
|
||||
|
||||
let logical_min = resolved.first().map(|r| r.logical).unwrap_or(0);
|
||||
let logical_max = resolved.last().map(|r| r.logical).unwrap_or(0);
|
||||
|
||||
Ok(quote! {
|
||||
impl #type_name {
|
||||
const VALUES: &'static [(f32, Self)] = &[
|
||||
#(#values_const)*
|
||||
];
|
||||
|
||||
pub const LOGICAL_MIN: i32 = #logical_min;
|
||||
pub const LOGICAL_MAX: i32 = #logical_max;
|
||||
|
||||
pub fn from_nearest_i32(value: i32) -> Self {
|
||||
Self::from_nearest_f32(value as f32)
|
||||
}
|
||||
|
||||
fn from_nearest_f32(value: f32) -> Self {
|
||||
Self::VALUES
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
let da = (a.0 - value).abs();
|
||||
let db = (b.0 - value).abs();
|
||||
da.partial_cmp(&db).unwrap_or(::std::cmp::Ordering::Equal)
|
||||
})
|
||||
.map(|(_, v)| *v)
|
||||
.unwrap_or(Self::VALUES[0].1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_from_logical_impl(
|
||||
type_name: &Ident,
|
||||
resolved: &[Resolved],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let arms = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let logical = r.logical;
|
||||
Ok(quote! { #logical => Ok(Self::#v), })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::convert::TryFrom<i32> for #type_name {
|
||||
type Error = ::anyhow::Error;
|
||||
fn try_from(value: i32) -> ::anyhow::Result<Self> {
|
||||
match value {
|
||||
#(#arms)*
|
||||
_ => ::anyhow::bail!(
|
||||
"Value {} is not a valid {}",
|
||||
value, stringify!(#type_name),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_to_logical_impl(
|
||||
type_name: &Ident,
|
||||
resolved: &[Resolved],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let arms = resolved
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let v = &r.ident;
|
||||
let logical = r.logical;
|
||||
Ok(quote! { #type_name::#v => #logical, })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::convert::From<#type_name> for i32 {
|
||||
fn from(value: #type_name) -> i32 {
|
||||
match value {
|
||||
#(#arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_display_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
let n = i32::from(*self);
|
||||
if n == 0 { write!(f, "0") } else { write!(f, "{n:+}") }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_str_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::anyhow::Error;
|
||||
fn from_str(s: &str) -> ::anyhow::Result<Self> {
|
||||
let value = crate::input::CleanAlphanumeric::clean(&s)
|
||||
.parse::<f32>()
|
||||
.map_err(|e| ::anyhow::anyhow!("Invalid numeric value '{}': {}", s, e))?;
|
||||
let min = Self::LOGICAL_MIN as f32;
|
||||
let max = Self::LOGICAL_MAX as f32;
|
||||
if !(min..=max).contains(&value) {
|
||||
::anyhow::bail!(
|
||||
"{} value {} is out of range [{}, {}]",
|
||||
stringify!(#type_name), value, min, max,
|
||||
);
|
||||
}
|
||||
Ok(Self::from_nearest_f32(value))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_serde_impls(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::serde::Serialize for #type_name {
|
||||
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_i32(i32::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> ::serde::Deserialize<'de> for #type_name {
|
||||
fn deserialize<D: ::serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Self, D::Error> {
|
||||
let logical = i32::deserialize(deserializer)?;
|
||||
<Self as ::std::convert::TryFrom<i32>>::try_from(logical)
|
||||
.map_err(::serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_ptp_serde_impl(type_name: &Ident, repr_type: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::ptp_cursor::PtpSerialize for #type_name {
|
||||
fn try_into_ptp(&self) -> ::std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
<Self as ::ptp_cursor::PtpSerialize>::try_write_ptp(self, &mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> ::std::io::Result<()> {
|
||||
let raw: #repr_type = *self as #repr_type;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&raw, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::ptp_cursor::PtpDeserialize for #type_name {
|
||||
fn try_from_ptp(buf: &[u8]) -> ::std::io::Result<Self> {
|
||||
let mut cur = ::std::io::Cursor::new(buf);
|
||||
let val = <Self as ::ptp_cursor::PtpDeserialize>::try_read_ptp(&mut cur)?;
|
||||
::ptp_cursor::Read::expect_end(&mut cur)?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn try_read_ptp<R: ::ptp_cursor::Read>(cur: &mut R) -> ::std::io::Result<Self> {
|
||||
let raw = <#repr_type as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
Self::try_from_wire(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_simulation_setting_impl(
|
||||
type_name: &Ident,
|
||||
prop_code: u16,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::SimulationSetting for #type_name {
|
||||
fn prop_code() -> u16 { #prop_code }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_conversion_profile_impl(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
repr_type_32: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::ConversionProfileField for #type_name {
|
||||
fn try_write_conversion_profile_field_ptp(
|
||||
&self, buf: &mut Vec<u8>,
|
||||
) -> ::std::io::Result<()> {
|
||||
let raw: #repr_type = *self as #repr_type;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&#repr_type_32::from(raw), buf)
|
||||
}
|
||||
|
||||
fn try_read_conversion_profile_field_ptp<R: ::ptp_cursor::Read>(
|
||||
cur: &mut R,
|
||||
) -> ::std::io::Result<Self> {
|
||||
let extended = <#repr_type_32 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
let raw: #repr_type = extended.try_into().map_err(|_| {
|
||||
::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} value {} doesn't fit in {}",
|
||||
stringify!(#type_name),
|
||||
extended,
|
||||
stringify!(#repr_type),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Self::try_from_wire(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod lookup;
|
||||
pub mod scaled;
|
||||
@@ -0,0 +1,294 @@
|
||||
use anyhow::{Context, bail};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use super::super::common::{resolve_numeric_repr_signed, resolve_repr_type, resolve_repr_type_32};
|
||||
use crate::{
|
||||
ast::{NumericEncoding, NumericRules},
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
struct Bounds {
|
||||
min: i32,
|
||||
max: i32,
|
||||
step: i32,
|
||||
scale: i32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn resolve(
|
||||
id: &str,
|
||||
rules: Option<&NumericRules<i32>>,
|
||||
encoding: &NumericEncoding,
|
||||
) -> anyhow::Result<Self> {
|
||||
let min = rules.and_then(|r| r.min).unwrap_or(i32::MIN);
|
||||
let max = rules.and_then(|r| r.max).unwrap_or(i32::MAX);
|
||||
let step = rules.and_then(|r| r.step).unwrap_or(1);
|
||||
let scale: i32 = match encoding {
|
||||
NumericEncoding::Raw { .. } => 1,
|
||||
NumericEncoding::Scale { spec, .. } => spec.scale,
|
||||
NumericEncoding::Lookup { .. } => {
|
||||
bail!("integer-lookup option `{id}` should use the lookup generator");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
id: &str,
|
||||
prop_code: Option<u16>,
|
||||
rules: Option<&NumericRules<i32>>,
|
||||
encoding: &NumericEncoding,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let bounds = Bounds::resolve(id, rules, encoding)
|
||||
.with_context(|| format!("resolving bounds for integer option `{id}`"))?;
|
||||
|
||||
let signed = resolve_numeric_repr_signed(bounds.min, bounds.max)
|
||||
.with_context(|| format!("determining representation type for integer option `{id}`"))?;
|
||||
|
||||
let repr_type = resolve_repr_type(signed);
|
||||
let repr_type_32 = resolve_repr_type_32(signed);
|
||||
|
||||
let type_name = safe_upper_camel_case_ident(id);
|
||||
|
||||
let struct_def = generate_struct_def(&type_name, &repr_type)
|
||||
.with_context(|| format!("generating struct definition for integer option `{id}`"))?;
|
||||
let inherent_impl = generate_inherent_impl(&type_name, signed, &bounds)
|
||||
.with_context(|| format!("generating inherent impl for integer option `{id}`"))?;
|
||||
let try_from_impl = generate_try_from_impl(&type_name, bounds.step)
|
||||
.with_context(|| format!("generating TryFrom<i32> impl for integer option `{id}`"))?;
|
||||
let to_impl = generate_to_impl(&type_name).with_context(|| {
|
||||
format!("generating From<{type_name}> for i32 impl for integer option `{id}`")
|
||||
})?;
|
||||
let display_impl = generate_display_impl(&type_name)
|
||||
.with_context(|| format!("generating Display impl for integer option `{id}`"))?;
|
||||
let from_str_impl = generate_from_str_impl(&type_name)
|
||||
.with_context(|| format!("generating FromStr impl for integer option `{id}`"))?;
|
||||
let serde_impls = generate_serde_impls(&type_name)
|
||||
.with_context(|| format!("generating Serde impls for integer option `{id}`"))?;
|
||||
let simulation_setting_impl = if let Some(code) = prop_code {
|
||||
generate_simulation_setting_impl(&type_name, code).with_context(|| {
|
||||
format!("generating SimulationSetting impl for integer option `{id}`")
|
||||
})?
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let conversion_profile_impl =
|
||||
generate_conversion_profile_impl(&type_name, &repr_type, &repr_type_32).with_context(
|
||||
|| format!("generating ConversionProfileField impl for integer option `{id}`"),
|
||||
)?;
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#inherent_impl
|
||||
#try_from_impl
|
||||
#to_impl
|
||||
#display_impl
|
||||
#from_str_impl
|
||||
#serde_impls
|
||||
#simulation_setting_impl
|
||||
#conversion_profile_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_struct_def(type_name: &Ident, repr_type: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
ptp_macro::PtpSerialize,
|
||||
ptp_macro::PtpDeserialize,
|
||||
)]
|
||||
pub struct #type_name(#repr_type);
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(
|
||||
type_name: &Ident,
|
||||
signed: bool,
|
||||
bounds: &Bounds,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let Bounds {
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
scale,
|
||||
} = *bounds;
|
||||
|
||||
let logical = quote! {
|
||||
pub const MIN: i32 = #min;
|
||||
pub const MAX: i32 = #max;
|
||||
pub const STEP: i32 = #step;
|
||||
pub const SCALE: i32 = #scale;
|
||||
};
|
||||
|
||||
let raw = if signed {
|
||||
let raw_min: i16 = (min * scale).try_into()?;
|
||||
let raw_max: i16 = (max * scale).try_into()?;
|
||||
let raw_step: i16 = (step * scale).try_into()?;
|
||||
|
||||
quote! {
|
||||
pub const RAW_MIN: i16 = #raw_min;
|
||||
pub const RAW_MAX: i16 = #raw_max;
|
||||
pub const RAW_STEP: i16 = #raw_step;
|
||||
}
|
||||
} else {
|
||||
let raw_min: u16 = (min * scale).try_into()?;
|
||||
let raw_max: u16 = (max * scale).try_into()?;
|
||||
let raw_step: u16 = (step * scale).try_into()?;
|
||||
|
||||
quote! {
|
||||
pub const RAW_MIN: u16 = #raw_min;
|
||||
pub const RAW_MAX: u16 = #raw_max;
|
||||
pub const RAW_STEP: u16 = #raw_step;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #type_name {
|
||||
#logical
|
||||
#raw
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_from_impl(type_name: &Ident, step: i32) -> anyhow::Result<TokenStream> {
|
||||
let step_check = if step != 1 {
|
||||
quote! {
|
||||
if (value - Self::MIN) % Self::STEP != 0 {
|
||||
::anyhow::bail!(
|
||||
"{} value {} is not aligned to step {}",
|
||||
stringify!(#type_name), value, Self::STEP,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::convert::TryFrom<i32> for #type_name {
|
||||
type Error = ::anyhow::Error;
|
||||
fn try_from(value: i32) -> ::anyhow::Result<Self> {
|
||||
if !(Self::MIN..=Self::MAX).contains(&value) {
|
||||
::anyhow::bail!(
|
||||
"{} value {} is out of range [{}, {}]",
|
||||
stringify!(#type_name), value, Self::MIN, Self::MAX,
|
||||
);
|
||||
}
|
||||
#step_check
|
||||
let raw = value * Self::SCALE;
|
||||
let raw = raw.try_into()?;
|
||||
Ok(Self(raw))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_to_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::convert::From<#type_name> for i32 {
|
||||
fn from(value: #type_name) -> i32 {
|
||||
value.0 as i32 / #type_name::SCALE
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_display_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
write!(f, "{}", i32::from(*self))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_str_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::anyhow::Error;
|
||||
fn from_str(s: &str) -> ::anyhow::Result<Self> {
|
||||
let logical = crate::input::CleanAlphanumeric::clean(&s)
|
||||
.parse::<i32>()
|
||||
.map_err(|e| ::anyhow::anyhow!("Invalid numeric value '{}': {}", s, e))?;
|
||||
Self::try_from(logical)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_serde_impls(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::serde::Serialize for #type_name {
|
||||
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_i32(i32::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> ::serde::Deserialize<'de> for #type_name {
|
||||
fn deserialize<D: ::serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Self, D::Error> {
|
||||
let logical = i32::deserialize(deserializer)?;
|
||||
Self::try_from(logical).map_err(::serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_simulation_setting_impl(
|
||||
type_name: &Ident,
|
||||
prop_code: u16,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::SimulationSetting for #type_name {
|
||||
fn prop_code() -> u16 { #prop_code }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_conversion_profile_impl(
|
||||
type_name: &Ident,
|
||||
repr_type: &Ident,
|
||||
repr_type_32: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::ConversionProfileField for #type_name {
|
||||
fn try_write_conversion_profile_field_ptp(
|
||||
&self, buf: &mut Vec<u8>,
|
||||
) -> ::std::io::Result<()> {
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&#repr_type_32::from(self.0), buf)
|
||||
}
|
||||
|
||||
fn try_read_conversion_profile_field_ptp<R: ::ptp_cursor::Read>(
|
||||
cur: &mut R,
|
||||
) -> ::std::io::Result<Self> {
|
||||
let extended = <#repr_type_32 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
let raw: #repr_type = extended.try_into().map_err(|_| {
|
||||
::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} value {} doesn't fit in {}",
|
||||
stringify!(#type_name),
|
||||
extended,
|
||||
stringify!(#repr_type),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok(Self(raw))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
mod common;
|
||||
mod r#enum;
|
||||
mod float;
|
||||
mod integer;
|
||||
mod string;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::ast::{FujiOption, NumericEncoding, OptionSpec};
|
||||
|
||||
pub fn generate(options: &BTreeMap<String, FujiOption>) -> anyhow::Result<TokenStream> {
|
||||
let mut blocks = Vec::with_capacity(options.len());
|
||||
|
||||
for (id, opt) in options {
|
||||
let block = match &opt.spec {
|
||||
OptionSpec::Enum {
|
||||
rules, encoding, ..
|
||||
} => r#enum::generate(id, rules, encoding)
|
||||
.with_context(|| format!("generating enum option `{id}`"))?,
|
||||
OptionSpec::Integer {
|
||||
rules, encoding, ..
|
||||
} => match encoding {
|
||||
NumericEncoding::Lookup { spec, prop_code } => {
|
||||
integer::lookup::generate(id, *prop_code, spec)
|
||||
.with_context(|| format!("generating integer lookup option `{id}`"))?
|
||||
}
|
||||
NumericEncoding::Raw { prop_code, .. }
|
||||
| NumericEncoding::Scale { prop_code, .. } => {
|
||||
integer::scaled::generate(id, *prop_code, rules.as_ref(), encoding)
|
||||
.with_context(|| format!("generating integer option `{id}`"))?
|
||||
}
|
||||
},
|
||||
OptionSpec::Float {
|
||||
rules, encoding, ..
|
||||
} => match encoding {
|
||||
NumericEncoding::Lookup { spec, prop_code } => {
|
||||
float::lookup::generate(id, *prop_code, spec)
|
||||
.with_context(|| format!("generating float lookup option `{id}`"))?
|
||||
}
|
||||
NumericEncoding::Raw { prop_code, .. }
|
||||
| NumericEncoding::Scale { prop_code, .. } => {
|
||||
float::scaled::generate(id, *prop_code, rules.as_ref(), encoding)
|
||||
.with_context(|| format!("generating float option `{id}`"))?
|
||||
}
|
||||
},
|
||||
OptionSpec::String {
|
||||
rules, encoding, ..
|
||||
} => string::generate(id, rules.as_ref(), encoding)
|
||||
.with_context(|| format!("generating string option `{id}`"))?,
|
||||
};
|
||||
blocks.push(block);
|
||||
}
|
||||
|
||||
let tokens = quote! {
|
||||
//! Generated option types. Do not edit.
|
||||
|
||||
#(#blocks)*
|
||||
};
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
pub fn path() -> TokenStream {
|
||||
quote! { crate::generated::options }
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
|
||||
use crate::{
|
||||
ast::{StringEncoding, StringRules},
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
struct Bounds {
|
||||
min_len: Option<u32>,
|
||||
max_len: Option<u32>,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn resolve(rules: Option<&StringRules>) -> Self {
|
||||
Self {
|
||||
min_len: rules.and_then(|r| r.min_length),
|
||||
max_len: rules.and_then(|r| r.max_length),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
id: &str,
|
||||
rules: Option<&StringRules>,
|
||||
encoding: &StringEncoding,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let StringEncoding::Raw { prop_code } = encoding;
|
||||
|
||||
let bounds = Bounds::resolve(rules);
|
||||
let type_name = safe_upper_camel_case_ident(id);
|
||||
|
||||
let struct_def = generate_struct_def(&type_name)
|
||||
.with_context(|| format!("generating struct definition for string option `{id}`"))?;
|
||||
let inherent_impl = generate_inherent_impl(&type_name, &bounds)
|
||||
.with_context(|| format!("generating inherent impl for string option `{id}`"))?;
|
||||
let from_str_impl = generate_from_str_impl(&type_name, &bounds)
|
||||
.with_context(|| format!("generating FromStr impl for string option `{id}`"))?;
|
||||
let display_impl = generate_display_impl(&type_name)
|
||||
.with_context(|| format!("generating Display impl for string option `{id}`"))?;
|
||||
let simulation_setting_impl = if let Some(code) = prop_code {
|
||||
generate_simulation_setting_impl(&type_name, *code).with_context(|| {
|
||||
format!("generating SimulationSetting impl for string option `{id}`")
|
||||
})?
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#inherent_impl
|
||||
#from_str_impl
|
||||
#display_impl
|
||||
#simulation_setting_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_struct_def(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
ptp_macro::PtpSerialize,
|
||||
ptp_macro::PtpDeserialize,
|
||||
serde_with::SerializeDisplay,
|
||||
serde_with::DeserializeFromStr,
|
||||
)]
|
||||
pub struct #type_name(String);
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(type_name: &Ident, bounds: &Bounds) -> anyhow::Result<TokenStream> {
|
||||
let const_block = match (bounds.min_len, bounds.max_len) {
|
||||
(Some(min), Some(max)) => quote! {
|
||||
pub const MIN_LEN: usize = #min as usize;
|
||||
pub const MAX_LEN: usize = #max as usize;
|
||||
},
|
||||
(None, Some(max)) => quote! {
|
||||
pub const MAX_LEN: usize = #max as usize;
|
||||
},
|
||||
(Some(min), None) => quote! {
|
||||
pub const MIN_LEN: usize = #min as usize;
|
||||
},
|
||||
(None, None) => quote! {},
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #type_name {
|
||||
#const_block
|
||||
|
||||
pub fn as_str(&self) -> &str { &self.0 }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_from_str_impl(type_name: &Ident, bounds: &Bounds) -> anyhow::Result<TokenStream> {
|
||||
let validate_min = bounds.min_len.map(|min| {
|
||||
quote! {
|
||||
if s.chars().count() < #min as usize {
|
||||
::anyhow::bail!(
|
||||
"{} value '{s}' is shorter than min length {}",
|
||||
stringify!(#type_name), #min,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let validate_max = bounds.max_len.map(|max| {
|
||||
quote! {
|
||||
if s.chars().count() > #max as usize {
|
||||
::anyhow::bail!(
|
||||
"{} value '{s}' exceeds max length {}",
|
||||
stringify!(#type_name), #max,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(quote! {
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::anyhow::Error;
|
||||
fn from_str(s: &str) -> ::anyhow::Result<Self> {
|
||||
#validate_min
|
||||
#validate_max
|
||||
Ok(Self(s.to_string()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_display_impl(type_name: &Ident) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_simulation_setting_impl(
|
||||
type_name: &Ident,
|
||||
prop_code: u16,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(quote! {
|
||||
impl crate::ptp::option::SimulationSetting for #type_name {
|
||||
fn prop_code() -> u16 { #prop_code }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::{
|
||||
ast::{Camera, FujiOption, SpecKind},
|
||||
common::simulations,
|
||||
schema::grammar::build_settings,
|
||||
};
|
||||
|
||||
struct UnionEntry {
|
||||
id: String,
|
||||
type_path: TokenStream,
|
||||
is_copy: bool,
|
||||
}
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let render_union = build_union(options, cameras)?;
|
||||
let simulation_field_ids = collect_simulation_field_ids(cameras);
|
||||
|
||||
let struct_def = generate_struct_def(&render_union);
|
||||
let merge_impl = generate_merge_impl(&render_union);
|
||||
let apply_simulation_impl =
|
||||
generate_apply_simulation_impl(&render_union, &simulation_field_ids);
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#merge_impl
|
||||
#apply_simulation_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn build_union(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<Vec<UnionEntry>> {
|
||||
let by_id = cameras
|
||||
.values()
|
||||
.filter_map(|camera| camera.spec.features.as_ref()?.render.as_ref())
|
||||
.try_fold(
|
||||
BTreeMap::<String, UnionEntry>::new(),
|
||||
|mut by_id, render| -> anyhow::Result<_> {
|
||||
let settings = build_settings(options, &render.fields)?;
|
||||
render.fields.iter().for_each(|field| {
|
||||
let id = field.id().to_string();
|
||||
let info = settings
|
||||
.get(id.as_str())
|
||||
.expect("ctx was just built from these fields");
|
||||
by_id.entry(id.clone()).or_insert_with(|| UnionEntry {
|
||||
id,
|
||||
type_path: info.type_path(),
|
||||
is_copy: !matches!(info.kind, SpecKind::String),
|
||||
});
|
||||
});
|
||||
Ok(by_id)
|
||||
},
|
||||
)?;
|
||||
Ok(by_id.into_values().collect())
|
||||
}
|
||||
|
||||
fn collect_simulation_field_ids(cameras: &BTreeMap<String, Camera>) -> BTreeSet<String> {
|
||||
cameras
|
||||
.values()
|
||||
.filter_map(|camera| camera.spec.features.as_ref()?.simulation.as_ref())
|
||||
.flat_map(|simulation| simulation.settings.iter().map(|s| s.id.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_struct_def(union: &[UnionEntry]) -> TokenStream {
|
||||
let fields = union.iter().map(|entry| {
|
||||
let ident = format_ident!("{}", entry.id);
|
||||
let ty = &entry.type_path;
|
||||
quote! {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub #ident: Option<#ty>,
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct RenderBase {
|
||||
#( #fields )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_merge_impl(union: &[UnionEntry]) -> TokenStream {
|
||||
let assigns = union.iter().map(|entry| {
|
||||
let ident = format_ident!("{}", entry.id);
|
||||
quote! {
|
||||
if let Some(value) = overlay.#ident {
|
||||
self.#ident = Some(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl RenderBase {
|
||||
pub fn merge(&mut self, overlay: Self) {
|
||||
#( #assigns )*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_apply_simulation_impl(
|
||||
union: &[UnionEntry],
|
||||
simulation_field_ids: &BTreeSet<String>,
|
||||
) -> TokenStream {
|
||||
let simulations_path = simulations::path();
|
||||
let assigns = union
|
||||
.iter()
|
||||
.filter(|entry| simulation_field_ids.contains(&entry.id))
|
||||
.map(|entry| {
|
||||
let ident = format_ident!("{}", entry.id);
|
||||
let access = if entry.is_copy {
|
||||
quote! { simulation.#ident }
|
||||
} else {
|
||||
quote! { simulation.#ident.clone() }
|
||||
};
|
||||
|
||||
quote! {
|
||||
if let Some(value) = #access {
|
||||
self.#ident = Some(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl RenderBase {
|
||||
pub fn try_update_from(
|
||||
&mut self,
|
||||
simulation: &#simulations_path::SimulationBase,
|
||||
) {
|
||||
#( #assigns )*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, Literal, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::{
|
||||
ast::{Camera, Dnf, Field, FujiOption, Render, Transformation},
|
||||
common::{cameras, renders},
|
||||
schema::{
|
||||
alias::{NormalizedRule, NormalizedTransformation},
|
||||
grammar::{
|
||||
SettingInfo, build_settings, generate_apply_transformations, generate_dnf,
|
||||
generate_emit_warnings_and_infos,
|
||||
},
|
||||
inverse::generate_inverses,
|
||||
presence::PresenceDag,
|
||||
repair::{generate_pin_set, generate_solve},
|
||||
},
|
||||
util::{dag::Dag, ident::safe_upper_camel_case_ident},
|
||||
};
|
||||
|
||||
// NOTE: Naively assume the same padding holds for all Fujifilm cameras
|
||||
// until we have a second render-capable camera to compare against.
|
||||
const RENDER_HEADER_PADDING: usize = 0x1EE;
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let mut blocks = Vec::with_capacity(cameras.len());
|
||||
for camera in cameras.values() {
|
||||
let block = generate_one(options, camera)
|
||||
.with_context(|| format!("generating render profile for camera `{}`", camera.id))?;
|
||||
blocks.push(block);
|
||||
}
|
||||
Ok(quote! { #( #blocks )* })
|
||||
}
|
||||
|
||||
fn generate_one(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
camera: &Camera,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let Some(render) = camera
|
||||
.spec
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|f| f.render.as_ref())
|
||||
else {
|
||||
return Ok(quote! {});
|
||||
};
|
||||
|
||||
let settings = build_settings(options, &render.fields)?;
|
||||
|
||||
let aliases: Vec<NormalizedTransformation> = render
|
||||
.transformations
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(Option::from)
|
||||
.collect();
|
||||
let effective_rules: Vec<NormalizedRule> = render
|
||||
.rules
|
||||
.iter()
|
||||
.map(|r| NormalizedRule::from_rule(r, &aliases))
|
||||
.collect();
|
||||
|
||||
let struct_ident = format_ident!("{}RenderProfile", safe_upper_camel_case_ident(&camera.id));
|
||||
let camera_struct_ident = safe_upper_camel_case_ident(&camera.id);
|
||||
let cameras_path = cameras::path();
|
||||
let camera_struct_path = quote! { #cameras_path::#camera_struct_ident };
|
||||
let renders_path = renders::path();
|
||||
|
||||
let presence_info = PresenceDag::try_from_rules(&effective_rules)
|
||||
.with_context(|| format!("extracting read DAG for `{}`", camera.id))?;
|
||||
|
||||
let nodes: Vec<&str> = render.fields.iter().map(Field::id).collect();
|
||||
let edges: Vec<(&str, &str)> = presence_info
|
||||
.edges
|
||||
.iter()
|
||||
.map(|(from, to)| (from.as_str(), to.as_str()))
|
||||
.collect();
|
||||
|
||||
let convert_order: Vec<String> = Dag::new(nodes, edges)
|
||||
.topological_order()?
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
|
||||
let n_props = i16::try_from(render.fields.len())
|
||||
.with_context(|| format!("too many render fields on camera `{}`", camera.id))?;
|
||||
let profile_code = render.profile_code;
|
||||
|
||||
let struct_def = generate_struct_def(&settings, &render.fields, &struct_ident);
|
||||
let inherent_impl = generate_inherent_impl(
|
||||
&settings,
|
||||
render,
|
||||
&effective_rules,
|
||||
&struct_ident,
|
||||
&renders_path,
|
||||
profile_code,
|
||||
)?;
|
||||
let serialize_impl =
|
||||
generate_ptp_serialize_impl(&settings, &render.fields, &struct_ident, n_props);
|
||||
let deserialize_impl = generate_ptp_deserialize_impl(
|
||||
&settings,
|
||||
&render.fields,
|
||||
&struct_ident,
|
||||
n_props,
|
||||
&presence_info.conditions,
|
||||
&render.transformations,
|
||||
&convert_order,
|
||||
)?;
|
||||
let trait_impl =
|
||||
generate_camera_render_manager_impl(&struct_ident, &camera_struct_path, &renders_path);
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#inherent_impl
|
||||
#serialize_impl
|
||||
#deserialize_impl
|
||||
#trait_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_struct_def(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Field],
|
||||
struct_ident: &Ident,
|
||||
) -> TokenStream {
|
||||
let field_defs = fields.iter().map(|f| {
|
||||
let info = settings.get(f.id()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let type_path = info.type_path();
|
||||
quote! { pub #ident: Option<#type_path>, }
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct #struct_ident {
|
||||
#( #field_defs )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
render: &Render,
|
||||
effective_rules: &[NormalizedRule],
|
||||
struct_ident: &Ident,
|
||||
renders_path: &TokenStream,
|
||||
profile_code: u32,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let profile_code_lit = Literal::u32_suffixed(profile_code);
|
||||
|
||||
let apply_transformations = generate_apply_transformations(settings, &render.transformations)?;
|
||||
let warnings_infos = generate_emit_warnings_and_infos(settings, effective_rules)?;
|
||||
let solve = generate_solve(settings, effective_rules)?;
|
||||
let try_update_from =
|
||||
generate_try_update_from(settings, &render.fields, renders_path, struct_ident)?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #struct_ident {
|
||||
pub const PROFILE_CODE: u32 = #profile_code_lit;
|
||||
|
||||
#apply_transformations
|
||||
#warnings_infos
|
||||
#solve
|
||||
#try_update_from
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_update_from(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Field],
|
||||
renders_path: &TokenStream,
|
||||
struct_ident: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let init_fields = fields.iter().map(|f| {
|
||||
let info = settings.get(f.id()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
quote! { #ident: partial.#ident, }
|
||||
});
|
||||
|
||||
let merge_assigns = fields.iter().map(|f| {
|
||||
let info = settings.get(f.id()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
quote! {
|
||||
if let Some(value) = partial_profile.#ident.take() {
|
||||
candidate.#ident = Some(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let pin_set_expr = generate_pin_set(settings, "e! { partial_profile });
|
||||
|
||||
Ok(quote! {
|
||||
pub fn try_update_from(
|
||||
&mut self,
|
||||
partial: #renders_path::RenderBase,
|
||||
) -> ::anyhow::Result<()> {
|
||||
let mut partial_profile: #struct_ident = #struct_ident {
|
||||
#( #init_fields )*
|
||||
};
|
||||
partial_profile.apply_transformations();
|
||||
|
||||
let pin = #pin_set_expr;
|
||||
|
||||
let mut candidate = self.clone();
|
||||
#( #merge_assigns )*
|
||||
candidate.apply_transformations();
|
||||
|
||||
candidate.solve(&pin)?;
|
||||
candidate.emit_warnings_and_infos()?;
|
||||
|
||||
*self = candidate;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_ptp_serialize_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Field],
|
||||
struct_ident: &Ident,
|
||||
n_props: i16,
|
||||
) -> TokenStream {
|
||||
let n_props_lit = Literal::i16_suffixed(n_props);
|
||||
let padding_lit = Literal::usize_suffixed(RENDER_HEADER_PADDING);
|
||||
|
||||
let writes = fields
|
||||
.iter()
|
||||
.map(|field| generate_write_one(settings, field));
|
||||
|
||||
quote! {
|
||||
impl ::ptp_cursor::PtpSerialize for #struct_ident {
|
||||
fn try_into_ptp(&self) -> ::std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
<Self as ::ptp_cursor::PtpSerialize>::try_write_ptp(self, &mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> ::std::io::Result<()> {
|
||||
let n_props: i16 = #n_props_lit;
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&n_props, buf)?;
|
||||
let profile_code = ::ptp_cursor::ExactString::from(
|
||||
format!("{:x}", Self::PROFILE_CODE),
|
||||
);
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&profile_code, buf)?;
|
||||
let padding = [0u8; #padding_lit];
|
||||
::std::io::Write::write_all(buf, &padding)?;
|
||||
|
||||
#( #writes )*
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_write_one(settings: &BTreeMap<&str, SettingInfo<'_>>, field: &Field) -> TokenStream {
|
||||
if field.skip_write() {
|
||||
return quote! {};
|
||||
}
|
||||
let info = settings.get(field.id()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let type_path = info.type_path();
|
||||
if info.option.is_some() {
|
||||
quote! {
|
||||
match self.#ident.as_ref() {
|
||||
Some(value) => {
|
||||
<#type_path as crate::ptp::option::ConversionProfileField>
|
||||
::try_write_conversion_profile_field_ptp(value, buf)?;
|
||||
}
|
||||
None => {
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&0i32, buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
let value: i32 = self.#ident.unwrap_or(0);
|
||||
::ptp_cursor::PtpSerialize::try_write_ptp(&value, buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_ptp_deserialize_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Field],
|
||||
struct_ident: &Ident,
|
||||
n_props: i16,
|
||||
presence_conditions: &BTreeMap<String, Dnf>,
|
||||
transformations: &[Transformation],
|
||||
convert_order: &[String],
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let n_props_lit = Literal::i16_suffixed(n_props);
|
||||
let padding_lit = Literal::usize_suffixed(RENDER_HEADER_PADDING);
|
||||
|
||||
let raw_reads: Vec<TokenStream> = fields
|
||||
.iter()
|
||||
.filter(|f| !f.skip_read())
|
||||
.map(|field| {
|
||||
let info = settings.get(field.id()).expect("settings indexed");
|
||||
let raw_ident = raw_local_ident(&info.field_ident());
|
||||
quote! {
|
||||
let #raw_ident = <i32 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let conversions = convert_order
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let field = fields
|
||||
.iter()
|
||||
.find(|f| f.id() == id.as_str())
|
||||
.expect("convert order references known field");
|
||||
generate_convert_one(settings, field, presence_conditions)
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
let inverses = generate_inverses(settings, transformations, "e! { profile })?;
|
||||
|
||||
Ok(quote! {
|
||||
impl ::ptp_cursor::PtpDeserialize for #struct_ident {
|
||||
fn try_from_ptp(buf: &[u8]) -> ::std::io::Result<Self> {
|
||||
let mut cur = ::std::io::Cursor::new(buf);
|
||||
let val = <Self as ::ptp_cursor::PtpDeserialize>::try_read_ptp(&mut cur)?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
fn try_read_ptp<R: ::ptp_cursor::Read>(
|
||||
cur: &mut R,
|
||||
) -> ::std::io::Result<Self> {
|
||||
let n_props = <i16 as ::ptp_cursor::PtpDeserialize>::try_read_ptp(cur)?;
|
||||
if n_props != #n_props_lit {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: expected {} props on the wire, got {}",
|
||||
stringify!(#struct_ident),
|
||||
#n_props_lit,
|
||||
n_props,
|
||||
),
|
||||
));
|
||||
}
|
||||
let profile_code_str =
|
||||
<::ptp_cursor::ExactString as ::ptp_cursor::PtpDeserialize>
|
||||
::try_read_ptp(cur)?;
|
||||
let parsed = u32::from_str_radix(profile_code_str.as_str(), 16)
|
||||
.map_err(|err| ::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: invalid profile-code hex `{}`: {}",
|
||||
stringify!(#struct_ident),
|
||||
profile_code_str.as_str(),
|
||||
err,
|
||||
),
|
||||
))?;
|
||||
if parsed != Self::PROFILE_CODE {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: expected profile code {:#x}, got {:#x}",
|
||||
stringify!(#struct_ident),
|
||||
Self::PROFILE_CODE,
|
||||
parsed,
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut padding = [0u8; #padding_lit];
|
||||
<R as ::std::io::Read>::read_exact(cur, &mut padding)?;
|
||||
|
||||
#( #raw_reads )*
|
||||
|
||||
let mut profile = Self::default();
|
||||
#( #conversions )*
|
||||
|
||||
#inverses
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_convert_one(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
field: &Field,
|
||||
presence_conditions: &BTreeMap<String, Dnf>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
if field.skip_read() {
|
||||
return Ok(quote! {});
|
||||
}
|
||||
|
||||
let info = settings.get(field.id()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let type_path = info.type_path();
|
||||
let raw_ident = raw_local_ident(&ident);
|
||||
|
||||
let convert = if info.option.is_some() {
|
||||
quote! {
|
||||
profile.#ident = Some(
|
||||
<#type_path as crate::ptp::option::ConversionProfileField>
|
||||
::try_from_conversion_profile_field_ptp(&#raw_ident.to_le_bytes())?,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
quote! { profile.#ident = Some(#raw_ident); }
|
||||
};
|
||||
|
||||
if let Some(condition) = presence_conditions.get(field.id()) {
|
||||
let profile_accessor = quote! { profile };
|
||||
let cond = generate_dnf(settings, condition, &profile_accessor)?;
|
||||
Ok(quote! {
|
||||
if #cond {
|
||||
#convert
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Ok(convert)
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_local_ident(ident: &Ident) -> Ident {
|
||||
format_ident!("raw_{}", ident)
|
||||
}
|
||||
|
||||
fn generate_camera_render_manager_impl(
|
||||
struct_ident: &Ident,
|
||||
camera_struct_path: &TokenStream,
|
||||
renders_path: &TokenStream,
|
||||
) -> TokenStream {
|
||||
quote! {
|
||||
impl crate::features::render::CameraRenderManager for #camera_struct_path {
|
||||
fn render(
|
||||
&self,
|
||||
ptp: &mut crate::ptp::Ptp,
|
||||
image: &[u8],
|
||||
partial: #renders_path::RenderBase,
|
||||
draft: bool,
|
||||
) -> ::anyhow::Result<Vec<u8>> {
|
||||
<Self as crate::features::render::CameraRenderManager>::send_image(self, ptp, image)?;
|
||||
let mut profile: #struct_ident = ptp.get_prop(
|
||||
crate::ptp::DevicePropCode::FujiRawConversionProfile,
|
||||
)?;
|
||||
profile.try_update_from(partial)?;
|
||||
ptp.set_prop(
|
||||
crate::ptp::DevicePropCode::FujiRawConversionProfile,
|
||||
&profile,
|
||||
)?;
|
||||
<Self as crate::features::render::CameraRenderManager>::render_image(
|
||||
self, ptp, draft,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
pub mod base;
|
||||
pub mod camera;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::ast::{Camera, FujiOption};
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let base_struct = base::generate(options, cameras).context("generating RenderBase")?;
|
||||
let per_camera =
|
||||
camera::generate(options, cameras).context("generating per-camera render structs")?;
|
||||
|
||||
Ok(quote! {
|
||||
//! Generated render profile types. Do not edit.
|
||||
|
||||
#base_struct
|
||||
#per_camera
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path() -> TokenStream {
|
||||
quote! { crate::generated::renders }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::{
|
||||
ast::{Camera, FujiOption},
|
||||
schema::grammar::build_settings,
|
||||
};
|
||||
|
||||
struct UnionEntry {
|
||||
id: String,
|
||||
type_path: TokenStream,
|
||||
}
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let union = build_union(options, cameras)?;
|
||||
Ok(generate_struct_def(&union))
|
||||
}
|
||||
|
||||
fn build_union(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<Vec<UnionEntry>> {
|
||||
let by_id = cameras
|
||||
.values()
|
||||
.filter_map(|camera| camera.spec.features.as_ref()?.simulation.as_ref())
|
||||
.try_fold(
|
||||
BTreeMap::<String, UnionEntry>::new(),
|
||||
|mut by_id, simulation| -> anyhow::Result<_> {
|
||||
let settings = build_settings(options, &simulation.settings)?;
|
||||
simulation.settings.iter().for_each(|setting| {
|
||||
let id = setting.id.clone();
|
||||
let info = settings
|
||||
.get(id.as_str())
|
||||
.expect("ctx was just built from these settings");
|
||||
by_id.entry(id.clone()).or_insert_with(|| UnionEntry {
|
||||
id,
|
||||
type_path: info.type_path(),
|
||||
});
|
||||
});
|
||||
Ok(by_id)
|
||||
},
|
||||
)?;
|
||||
Ok(by_id.into_values().collect())
|
||||
}
|
||||
|
||||
fn generate_struct_def(union: &[UnionEntry]) -> TokenStream {
|
||||
let base_fields = union.iter().map(|entry| {
|
||||
let ident = format_ident!("{}", entry.id);
|
||||
let ty = &entry.type_path;
|
||||
quote! {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub #ident: Option<#ty>,
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct SimulationBase {
|
||||
#( #base_fields )*
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::{
|
||||
ast::{Camera, Dnf, FujiOption, Leaf, Setting},
|
||||
common::{cameras, options},
|
||||
schema::{
|
||||
alias::{NormalizedRule, NormalizedTransformation},
|
||||
grammar::{
|
||||
SettingInfo, build_settings, generate_apply_transformations, generate_dnf,
|
||||
generate_emit_warnings_and_infos,
|
||||
},
|
||||
presence::PresenceDag,
|
||||
repair::{generate_pin_set, generate_solve},
|
||||
},
|
||||
util::{dag::Dag, ident::safe_upper_camel_case_ident},
|
||||
};
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let base_union = collect_base_union(cameras);
|
||||
let mut blocks = Vec::with_capacity(cameras.len());
|
||||
for camera in cameras.values() {
|
||||
let block = generate_one(options, camera, &base_union)
|
||||
.with_context(|| format!("generating simulation for camera `{}`", camera.id))?;
|
||||
blocks.push(block);
|
||||
}
|
||||
Ok(quote! { #( #blocks )* })
|
||||
}
|
||||
|
||||
fn collect_base_union(cameras: &BTreeMap<String, Camera>) -> BTreeSet<String> {
|
||||
cameras
|
||||
.values()
|
||||
.filter_map(|c| c.spec.features.as_ref()?.simulation.as_ref())
|
||||
.flat_map(|s| s.settings.iter().map(|setting| setting.id.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_one(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
camera: &Camera,
|
||||
base_union: &BTreeSet<String>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let Some(simulation) = camera
|
||||
.spec
|
||||
.features
|
||||
.as_ref()
|
||||
.and_then(|f| f.simulation.as_ref())
|
||||
else {
|
||||
return Ok(quote! {});
|
||||
};
|
||||
|
||||
let settings = build_settings(options, &simulation.settings)?;
|
||||
|
||||
let aliases: Vec<NormalizedTransformation> = simulation
|
||||
.transformations
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(Option::from)
|
||||
.collect();
|
||||
let effective_rules: Vec<NormalizedRule> = simulation
|
||||
.rules
|
||||
.iter()
|
||||
.map(|r| NormalizedRule::from_rule(r, &aliases))
|
||||
.collect();
|
||||
|
||||
let struct_ident = format_ident!("{}Simulation", safe_upper_camel_case_ident(&camera.id));
|
||||
let camera_struct_ident = safe_upper_camel_case_ident(&camera.id);
|
||||
let cameras_path = cameras::path();
|
||||
let camera_struct_path = quote! { #cameras_path::#camera_struct_ident };
|
||||
let options_path = options::path();
|
||||
|
||||
let presence_info = PresenceDag::try_from_rules(&effective_rules)
|
||||
.with_context(|| format!("extracting presence DAG for `{}`", camera.id))?;
|
||||
|
||||
let nodes: Vec<&str> = simulation.settings.iter().map(|s| s.id.as_str()).collect();
|
||||
let edges: Vec<(&str, &str)> = presence_info
|
||||
.edges
|
||||
.iter()
|
||||
.map(|(from, to)| (from.as_str(), to.as_str()))
|
||||
.collect();
|
||||
|
||||
let write_order: Vec<String> = Dag::new(nodes, edges)
|
||||
.topological_order()?
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
let read_order = write_order.clone();
|
||||
|
||||
let struct_def = generate_struct_def(&settings, &simulation.settings, &struct_ident);
|
||||
let inherent_impl = generate_inherent_impl(
|
||||
&settings,
|
||||
simulation,
|
||||
&effective_rules,
|
||||
&struct_ident,
|
||||
&options_path,
|
||||
)?;
|
||||
let from_sim_impl =
|
||||
generate_from_sim_for_base_impl(&settings, &simulation.settings, &struct_ident, base_union);
|
||||
let try_from_base_impl = generate_try_from_base_impl(
|
||||
&settings,
|
||||
&simulation.settings,
|
||||
&effective_rules,
|
||||
&struct_ident,
|
||||
);
|
||||
let display_impl = generate_display_impl(&settings, &simulation.settings, &struct_ident);
|
||||
let simulation_impl = generate_simulation_impl(
|
||||
&settings,
|
||||
&struct_ident,
|
||||
&options_path,
|
||||
&read_order,
|
||||
&write_order,
|
||||
&presence_info.conditions,
|
||||
)?;
|
||||
let parser_impl = generate_parser_impl(&struct_ident, &camera_struct_path);
|
||||
let manager_impl = generate_manager_impl(&struct_ident, &camera_struct_path, &options_path);
|
||||
|
||||
Ok(quote! {
|
||||
#struct_def
|
||||
#inherent_impl
|
||||
#from_sim_impl
|
||||
#try_from_base_impl
|
||||
#display_impl
|
||||
#simulation_impl
|
||||
#parser_impl
|
||||
#manager_impl
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_struct_def(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Setting],
|
||||
struct_ident: &Ident,
|
||||
) -> TokenStream {
|
||||
let field_defs = fields.iter().map(|s| {
|
||||
let info = settings.get(s.id.as_str()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let type_path = info.type_path();
|
||||
quote! {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub #ident: Option<#type_path>,
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct #struct_ident {
|
||||
#( #field_defs )*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_inherent_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
simulation: &crate::ast::Simulation,
|
||||
effective_rules: &[NormalizedRule],
|
||||
struct_ident: &Ident,
|
||||
options_path: &TokenStream,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let slots = simulation.slots;
|
||||
|
||||
let apply_transformations =
|
||||
generate_apply_transformations(settings, &simulation.transformations)?;
|
||||
let warnings_infos = generate_emit_warnings_and_infos(settings, effective_rules)?;
|
||||
let solve = generate_solve(settings, effective_rules)?;
|
||||
let try_update_from = generate_try_update_from(settings, &simulation.settings, struct_ident)?;
|
||||
let name = generate_name(settings, options_path);
|
||||
|
||||
Ok(quote! {
|
||||
impl #struct_ident {
|
||||
pub const SLOTS: u32 = #slots;
|
||||
|
||||
#apply_transformations
|
||||
#warnings_infos
|
||||
#solve
|
||||
#try_update_from
|
||||
#name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_update_from(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Setting],
|
||||
struct_ident: &Ident,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let init_fields = fields.iter().map(|s| {
|
||||
let info = settings.get(s.id.as_str()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
quote! { #ident: partial.#ident, }
|
||||
});
|
||||
|
||||
let merge_assigns = fields.iter().map(|s| {
|
||||
let info = settings.get(s.id.as_str()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
quote! {
|
||||
if let Some(value) = partial_profile.#ident.take() {
|
||||
candidate.#ident = Some(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let pin_set_expr = generate_pin_set(settings, "e! { partial_profile });
|
||||
|
||||
Ok(quote! {
|
||||
pub fn try_update_from(
|
||||
&mut self,
|
||||
partial: crate::generated::simulations::SimulationBase,
|
||||
) -> ::anyhow::Result<()> {
|
||||
let mut partial_profile: #struct_ident = #struct_ident {
|
||||
#( #init_fields )*
|
||||
};
|
||||
partial_profile.apply_transformations();
|
||||
|
||||
let pin = #pin_set_expr;
|
||||
|
||||
let mut candidate = self.clone();
|
||||
#( #merge_assigns )*
|
||||
candidate.apply_transformations();
|
||||
|
||||
candidate.solve(&pin)?;
|
||||
candidate.emit_warnings_and_infos()?;
|
||||
|
||||
*self = candidate;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_name(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
options_path: &TokenStream,
|
||||
) -> TokenStream {
|
||||
let body = if settings.contains_key("custom_setting_name") {
|
||||
quote! { self.custom_setting_name.clone() }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
quote! {
|
||||
pub fn name(&self) -> Option<#options_path::CustomSettingName> {
|
||||
#body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_from_sim_for_base_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Setting],
|
||||
struct_ident: &Ident,
|
||||
base_union: &BTreeSet<String>,
|
||||
) -> TokenStream {
|
||||
let init_fields = fields.iter().map(|s| {
|
||||
let info = settings.get(s.id.as_str()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let value = if matches!(info.kind, crate::ast::SpecKind::String) {
|
||||
quote! { simulation.#ident.clone() }
|
||||
} else {
|
||||
quote! { simulation.#ident }
|
||||
};
|
||||
quote! { #ident: #value, }
|
||||
});
|
||||
|
||||
let tail = if fields.len() == base_union.len() {
|
||||
TokenStream::new()
|
||||
} else {
|
||||
quote! { ..::std::default::Default::default() }
|
||||
};
|
||||
|
||||
quote! {
|
||||
impl ::std::convert::From<&#struct_ident>
|
||||
for crate::generated::simulations::SimulationBase
|
||||
{
|
||||
fn from(simulation: &#struct_ident) -> Self {
|
||||
Self {
|
||||
#( #init_fields )*
|
||||
#tail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_try_from_base_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Setting],
|
||||
rules: &[NormalizedRule],
|
||||
struct_ident: &Ident,
|
||||
) -> TokenStream {
|
||||
let mut optional_fields: BTreeSet<String> = BTreeSet::new();
|
||||
for rule in rules {
|
||||
for conj in &rule.when {
|
||||
for leaf in conj {
|
||||
if let Leaf::Present(p) = leaf
|
||||
&& !p.present
|
||||
{
|
||||
optional_fields.insert(p.r#ref.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let struct_name = struct_ident.to_string();
|
||||
let required_checks =
|
||||
generate_required_field_checks(settings, fields, &optional_fields, &struct_name);
|
||||
|
||||
quote! {
|
||||
impl ::std::convert::TryFrom<crate::generated::simulations::SimulationBase>
|
||||
for #struct_ident
|
||||
{
|
||||
type Error = ::anyhow::Error;
|
||||
fn try_from(
|
||||
base: crate::generated::simulations::SimulationBase,
|
||||
) -> ::anyhow::Result<Self> {
|
||||
let mut sim = Self::default();
|
||||
sim.try_update_from(base)?;
|
||||
#required_checks
|
||||
Ok(sim)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_required_field_checks(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Setting],
|
||||
optional: &BTreeSet<String>,
|
||||
struct_name: &str,
|
||||
) -> TokenStream {
|
||||
let parts = fields.iter().filter_map(|s| {
|
||||
let id = s.id.as_str();
|
||||
if optional.contains(id) {
|
||||
return None;
|
||||
}
|
||||
let info = settings.get(id).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let id_str = id.to_string();
|
||||
Some(quote! {
|
||||
if sim.#ident.is_none() {
|
||||
::anyhow::bail!(
|
||||
"{}: required setting `{}` is missing",
|
||||
#struct_name,
|
||||
#id_str,
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
quote! { #( #parts )* }
|
||||
}
|
||||
|
||||
fn generate_display_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
fields: &[Setting],
|
||||
struct_ident: &Ident,
|
||||
) -> TokenStream {
|
||||
let lines = fields.iter().map(|s| {
|
||||
let info = settings.get(s.id.as_str()).expect("settings indexed");
|
||||
let ident = info.field_ident();
|
||||
let label = info
|
||||
.option
|
||||
.map(|o| o.spec.name().to_string())
|
||||
.unwrap_or_else(|| info.id.to_string());
|
||||
let escaped = label.replace('{', "{{").replace('}', "}}");
|
||||
let fmt = format!("{escaped}: {{value}}");
|
||||
quote! {
|
||||
if let Some(value) = self.#ident.as_ref() {
|
||||
writeln!(f, #fmt)?;
|
||||
}
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
impl ::std::fmt::Display for #struct_ident {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
#( #lines )*
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_simulation_impl(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
struct_ident: &Ident,
|
||||
options_path: &TokenStream,
|
||||
read_order: &[String],
|
||||
write_order: &[String],
|
||||
presence_conditions: &BTreeMap<String, Dnf>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let try_pull = generate_try_pull(settings, read_order, presence_conditions)?;
|
||||
let try_push = generate_try_push(settings, write_order);
|
||||
|
||||
Ok(quote! {
|
||||
impl crate::features::simulation::Simulation for #struct_ident {
|
||||
fn as_any(&self) -> &dyn ::std::any::Any { self }
|
||||
|
||||
fn name(&self) -> Option<#options_path::CustomSettingName> {
|
||||
<Self>::name(self)
|
||||
}
|
||||
|
||||
fn try_update_from(
|
||||
&mut self,
|
||||
partial: crate::generated::simulations::SimulationBase,
|
||||
) -> ::anyhow::Result<()> {
|
||||
<Self>::try_update_from(self, partial)
|
||||
}
|
||||
|
||||
#try_pull
|
||||
#try_push
|
||||
|
||||
fn to_base(&self) -> crate::generated::simulations::SimulationBase {
|
||||
<crate::generated::simulations::SimulationBase as ::std::convert::From<&#struct_ident>>::from(self)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_pull(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
read_order: &[String],
|
||||
presence_conditions: &BTreeMap<String, Dnf>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let staging_accessor = quote! { staged };
|
||||
let reads = read_order
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let info = settings
|
||||
.get(id.as_str())
|
||||
.expect("read order references known setting");
|
||||
let ident = info.field_ident();
|
||||
let type_path = info.type_path();
|
||||
|
||||
let read_call = quote! {
|
||||
Some(<#type_path as crate::ptp::option::SimulationSetting>::try_pull(ptp)?)
|
||||
};
|
||||
|
||||
let body = if let Some(dnf) = presence_conditions.get(id) {
|
||||
let cond = generate_dnf(settings, dnf, &staging_accessor)?;
|
||||
quote! {
|
||||
if #cond { #read_call } else { None }
|
||||
}
|
||||
} else {
|
||||
read_call
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
staged.#ident = #body;
|
||||
})
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
fn try_pull(ptp: &mut crate::ptp::Ptp) -> ::anyhow::Result<Self> {
|
||||
let mut staged = Self::default();
|
||||
#( #reads )*
|
||||
Ok(staged)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_try_push(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
write_order: &[String],
|
||||
) -> TokenStream {
|
||||
let writes = write_order.iter().map(|id| {
|
||||
let info = settings
|
||||
.get(id.as_str())
|
||||
.expect("write order references known setting");
|
||||
let ident = info.field_ident();
|
||||
|
||||
quote! {
|
||||
if let Some(value) = self.#ident.as_ref() {
|
||||
crate::ptp::option::SimulationSetting::try_push(value, ptp)?;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn try_push(&self, ptp: &mut crate::ptp::Ptp) -> ::anyhow::Result<()> {
|
||||
#( #writes )*
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_parser_impl(struct_ident: &Ident, camera_struct_path: &TokenStream) -> TokenStream {
|
||||
quote! {
|
||||
impl crate::features::simulation::CameraSimulationParser for #camera_struct_path {
|
||||
fn deserialize_simulation(
|
||||
&self,
|
||||
data: &[u8],
|
||||
) -> ::anyhow::Result<Box<dyn crate::features::simulation::Simulation>> {
|
||||
let sim: #struct_ident = ::serde_json::from_slice(data)?;
|
||||
Ok(Box::new(sim))
|
||||
}
|
||||
|
||||
fn serialize_simulation(
|
||||
&self,
|
||||
simulation: &dyn crate::features::simulation::Simulation,
|
||||
) -> ::anyhow::Result<Vec<u8>> {
|
||||
Ok(::serde_json::to_vec(simulation)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_manager_impl(
|
||||
struct_ident: &Ident,
|
||||
camera_struct_path: &TokenStream,
|
||||
options_path: &TokenStream,
|
||||
) -> TokenStream {
|
||||
let struct_name = struct_ident.to_string();
|
||||
quote! {
|
||||
impl crate::features::simulation::CameraSimulationManager for #camera_struct_path {
|
||||
fn custom_settings_slots(&self) -> Vec<#options_path::CustomSetting> {
|
||||
<#options_path::CustomSetting as ::strum::IntoEnumIterator>::iter()
|
||||
.take(#struct_ident::SLOTS as usize)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_simulation(
|
||||
&self,
|
||||
ptp: &mut crate::ptp::Ptp,
|
||||
slot: #options_path::CustomSetting,
|
||||
) -> ::anyhow::Result<Box<dyn crate::features::simulation::Simulation>> {
|
||||
crate::ptp::option::SimulationSetting::try_push(&slot, ptp)?;
|
||||
Ok(Box::new(
|
||||
<#struct_ident as crate::features::simulation::Simulation>::try_pull(ptp)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn update_simulation(
|
||||
&self,
|
||||
ptp: &mut crate::ptp::Ptp,
|
||||
slot: #options_path::CustomSetting,
|
||||
partial: crate::generated::simulations::SimulationBase,
|
||||
) -> ::anyhow::Result<()> {
|
||||
crate::ptp::option::SimulationSetting::try_push(&slot, ptp)?;
|
||||
let mut current =
|
||||
<#struct_ident as crate::features::simulation::Simulation>::try_pull(ptp)?;
|
||||
current.try_update_from(partial)?;
|
||||
<#struct_ident as crate::features::simulation::Simulation>::try_push(¤t, ptp)
|
||||
}
|
||||
|
||||
fn set_simulation(
|
||||
&self,
|
||||
ptp: &mut crate::ptp::Ptp,
|
||||
slot: #options_path::CustomSetting,
|
||||
simulation: &dyn crate::features::simulation::Simulation,
|
||||
) -> ::anyhow::Result<()> {
|
||||
let sim = simulation
|
||||
.as_any()
|
||||
.downcast_ref::<#struct_ident>()
|
||||
.ok_or_else(|| ::anyhow::anyhow!(
|
||||
"Simulation type mismatch: expected {}", #struct_name
|
||||
))?;
|
||||
crate::ptp::option::SimulationSetting::try_push(&slot, ptp)?;
|
||||
<#struct_ident as crate::features::simulation::Simulation>::try_push(sim, ptp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
pub mod base;
|
||||
pub mod camera;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::ast::{Camera, FujiOption};
|
||||
|
||||
pub fn generate(
|
||||
options: &BTreeMap<String, FujiOption>,
|
||||
cameras: &BTreeMap<String, Camera>,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let base_struct = base::generate(options, cameras).context("generating SimulationBase")?;
|
||||
|
||||
let per_camera =
|
||||
camera::generate(options, cameras).context("generating per-camera simulation structs")?;
|
||||
|
||||
Ok(quote! {
|
||||
//! Generated simulation types. Do not edit.
|
||||
|
||||
#base_struct
|
||||
#per_camera
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path() -> TokenStream {
|
||||
quote! { crate::generated::simulations }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
pub mod ast;
|
||||
mod cli;
|
||||
mod common;
|
||||
mod schema;
|
||||
mod util;
|
||||
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::Context;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
pub fn generate(json: &str, out_dir: &Path) -> anyhow::Result<()> {
|
||||
let fml: ast::Fml = serde_json::from_str(json).context("parsing FML JSON")?;
|
||||
|
||||
if out_dir.exists() {
|
||||
std::fs::remove_dir_all(out_dir)
|
||||
.with_context(|| format!("clearing {}", out_dir.display()))?;
|
||||
}
|
||||
std::fs::create_dir_all(out_dir).with_context(|| format!("creating {}", out_dir.display()))?;
|
||||
|
||||
let options = common::options::generate(&fml.options).context("generating option types")?;
|
||||
write(out_dir, "options", options)?;
|
||||
|
||||
let cameras = common::cameras::generate(&fml.cameras).context("generating camera registry")?;
|
||||
write(out_dir, "cameras", cameras)?;
|
||||
|
||||
let simulations = common::simulations::generate(&fml.options, &fml.cameras)
|
||||
.context("generating simulation types")?;
|
||||
write(out_dir, "simulations", simulations)?;
|
||||
|
||||
let renders = common::renders::generate(&fml.options, &fml.cameras)
|
||||
.context("generating render profile types")?;
|
||||
write(out_dir, "renders", renders)?;
|
||||
|
||||
let cli = cli::generate(&fml.options, &fml.cameras).context("generating CLI args")?;
|
||||
write(out_dir, "cli", cli)?;
|
||||
|
||||
let mod_rs = root(&fml);
|
||||
write(out_dir, "mod", mod_rs)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn root(fml: &ast::Fml) -> TokenStream {
|
||||
let banner = format!(
|
||||
"Generated via codegen. Do not edit. \
|
||||
Inventory: {} cameras, {} options",
|
||||
fml.cameras.len(),
|
||||
fml.options.len(),
|
||||
);
|
||||
|
||||
quote! {
|
||||
#![doc = #banner]
|
||||
|
||||
pub mod cameras;
|
||||
pub mod options;
|
||||
pub mod simulations;
|
||||
pub mod renders;
|
||||
pub mod cli;
|
||||
}
|
||||
}
|
||||
|
||||
fn write(out_dir: &Path, name: &str, tokens: TokenStream) -> anyhow::Result<()> {
|
||||
let formatted =
|
||||
format(tokens).with_context(|| format!("formatting generated module `{name}`"))?;
|
||||
let path = out_dir.join(format!("{name}.rs"));
|
||||
fs::write(&path, formatted).with_context(|| format!("writing {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format(tokens: TokenStream) -> anyhow::Result<String> {
|
||||
let file: syn::File = syn::parse2(tokens).context("parsing generated TokenStream")?;
|
||||
Ok(prettyplease::unparse(&file))
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
use crate::ast::{Conjunction, Dnf, Leaf, Rule, Severity, Transformation};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NormalizedTransformation {
|
||||
pub trigger: Dnf,
|
||||
pub expansion: Conjunction,
|
||||
}
|
||||
|
||||
impl From<Transformation> for Option<NormalizedTransformation> {
|
||||
fn from(t: Transformation) -> Self {
|
||||
let when = t.when?;
|
||||
if t.apply.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let trigger = Dnf::from(when);
|
||||
if trigger.is_contradiction() {
|
||||
return None;
|
||||
}
|
||||
let expansion = Conjunction(t.apply.iter().map(Leaf::from).collect());
|
||||
Some(NormalizedTransformation { trigger, expansion })
|
||||
}
|
||||
}
|
||||
|
||||
impl Dnf {
|
||||
pub fn transform(self, alias: &NormalizedTransformation) -> Self {
|
||||
Self(self.into_iter().map(|c| c.transform(alias)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Conjunction {
|
||||
pub fn transform(mut self, alias: &NormalizedTransformation) -> Self {
|
||||
for disjunct in &alias.trigger {
|
||||
if self.contains_all(disjunct) {
|
||||
for lit in disjunct {
|
||||
if let Some(pos) = self.iter().position(|l| l == lit) {
|
||||
self.swap_remove(pos);
|
||||
}
|
||||
}
|
||||
self.extend(alias.expansion.clone());
|
||||
return self;
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NormalizedRule {
|
||||
pub severity: Severity,
|
||||
pub message: String,
|
||||
pub when: Dnf,
|
||||
}
|
||||
|
||||
impl NormalizedRule {
|
||||
pub fn from_rule(rule: &Rule, aliases: &[NormalizedTransformation]) -> Self {
|
||||
let initial = Dnf::from(rule.when.clone());
|
||||
let when = aliases.iter().fold(initial, Dnf::transform);
|
||||
Self {
|
||||
severity: rule.severity,
|
||||
message: rule.message.clone(),
|
||||
when,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{
|
||||
Assignment, AssignmentEffect, LeafEquals, LeafPresent, PredAll, PredNot, Predicate, Rule,
|
||||
Severity,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn normalize(
|
||||
rules: &[Rule],
|
||||
transformations: impl IntoIterator<Item = Transformation>,
|
||||
) -> Vec<NormalizedRule> {
|
||||
let aliases: Vec<NormalizedTransformation> = transformations
|
||||
.into_iter()
|
||||
.filter_map(Option::from)
|
||||
.collect();
|
||||
rules
|
||||
.iter()
|
||||
.map(|r| NormalizedRule::from_rule(r, &aliases))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn alias_t(when: Predicate, apply: Vec<(&str, Value)>) -> Transformation {
|
||||
Transformation {
|
||||
when: Some(when),
|
||||
apply: apply
|
||||
.into_iter()
|
||||
.map(|(r, v)| Assignment {
|
||||
r#ref: r.to_string(),
|
||||
effect: AssignmentEffect::Set(v),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rule(when: Predicate) -> Rule {
|
||||
Rule {
|
||||
severity: Severity::Error,
|
||||
message: "test".into(),
|
||||
when,
|
||||
}
|
||||
}
|
||||
|
||||
fn le(name: &str, v: Value) -> Leaf {
|
||||
Leaf::Equals(LeafEquals {
|
||||
r#ref: name.into(),
|
||||
equals: v,
|
||||
})
|
||||
}
|
||||
|
||||
fn lp(name: &str, present: bool) -> Leaf {
|
||||
Leaf::Present(LeafPresent {
|
||||
r#ref: name.into(),
|
||||
present,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_trigger_expands_to_apply_conjunction() {
|
||||
let ts = vec![alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
vec![("dr", json!("hdr800")), ("drp", json!("plus"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
assert_eq!(out[0].when.0.len(), 1);
|
||||
let conj = &out[0].when.0[0];
|
||||
assert_eq!(conj.0.len(), 2);
|
||||
assert!(conj.0.contains(&le("dr", json!("hdr800"))));
|
||||
assert!(conj.0.contains(&le("drp", json!("plus"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmatched_leaves_pass_through() {
|
||||
let ts = vec![alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
vec![("dr", json!("hdr800")), ("drp", json!("plus"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "film_simulation".into(),
|
||||
equals: json!("provia"),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
assert_eq!(
|
||||
out[0].when.0[0].0,
|
||||
vec![le("film_simulation", json!("provia"))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_in_apply_expands_to_present_false() {
|
||||
let ts = vec![Transformation {
|
||||
when: Some(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
apply: vec![Assignment {
|
||||
r#ref: "wb_shift_red".into(),
|
||||
effect: AssignmentEffect::Clear,
|
||||
}],
|
||||
}];
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
assert_eq!(out[0].when.0[0].0, vec![lp("wb_shift_red", false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_set_and_clear_apply_produces_conjunction() {
|
||||
let ts = vec![Transformation {
|
||||
when: Some(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
apply: vec![
|
||||
Assignment {
|
||||
r#ref: "wb_lock".into(),
|
||||
effect: AssignmentEffect::Set(json!(true)),
|
||||
},
|
||||
Assignment {
|
||||
r#ref: "wb_shift_red".into(),
|
||||
effect: AssignmentEffect::Clear,
|
||||
},
|
||||
],
|
||||
}];
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "wb".into(),
|
||||
equals: json!("as_shot"),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
let conj = &out[0].when.0[0];
|
||||
assert!(conj.0.contains(&le("wb_lock", json!(true))));
|
||||
assert!(conj.0.contains(&lp("wb_shift_red", false)));
|
||||
assert_eq!(conj.0.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_triggers_apply_first_only_per_conjunction() {
|
||||
let ts = vec![
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
vec![("dr", json!("hdr800"))],
|
||||
),
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
vec![("drp", json!("plus"))],
|
||||
),
|
||||
];
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
// First alias substitutes dr -> hdr800; second alias finds no
|
||||
// hdr800_plus match in the rewritten conjunction.
|
||||
assert_eq!(out[0].when.0[0].0, vec![le("dr", json!("hdr800"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substitution_recurses_into_logic_nodes() {
|
||||
let ts = vec![alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
vec![("dr", json!("hdr800")), ("drp", json!("plus"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "dr".into(),
|
||||
equals: json!("hdr800_plus"),
|
||||
}
|
||||
.into(),
|
||||
PredNot {
|
||||
not: Box::new(
|
||||
LeafEquals {
|
||||
r#ref: "foo".into(),
|
||||
equals: json!("bar"),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
let conj = &out[0].when.0[0];
|
||||
assert!(conj.0.iter().any(|l| matches!(l, Leaf::NotEquals(_))));
|
||||
assert!(conj.0.contains(&le("dr", json!("hdr800"))));
|
||||
assert!(conj.0.contains(&le("drp", json!("plus"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_when_trigger_is_recognised() {
|
||||
let ts = vec![alias_t(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
vec![("x", json!("v1")), ("y", json!("v2"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
let conj = &out[0].when.0[0];
|
||||
assert!(conj.0.contains(&le("x", json!("v1"))));
|
||||
assert!(conj.0.contains(&le("y", json!("v2"))));
|
||||
assert_eq!(conj.0.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_match_is_order_insensitive_under_all() {
|
||||
let ts = vec![alias_t(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
vec![("x", json!("v"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafPresent {
|
||||
r#ref: "b".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
assert_eq!(out[0].when.0[0].0, vec![le("x", json!("v"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_trigger_does_not_match_negated_rule() {
|
||||
let ts = vec![alias_t(
|
||||
LeafPresent {
|
||||
r#ref: "legacy_field".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
vec![("modern_field", json!("default"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
PredNot {
|
||||
not: Box::new(
|
||||
LeafPresent {
|
||||
r#ref: "legacy_field".into(),
|
||||
present: true,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
assert_eq!(out[0].when.0[0].0, vec![lp("legacy_field", false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chained_aliases_substitute_in_declaration_order() {
|
||||
let ts = vec![
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
vec![("b", json!("y"))],
|
||||
),
|
||||
alias_t(
|
||||
LeafEquals {
|
||||
r#ref: "b".into(),
|
||||
equals: json!("y"),
|
||||
}
|
||||
.into(),
|
||||
vec![("c", json!("z"))],
|
||||
),
|
||||
];
|
||||
let rules = vec![rule(
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!("x"),
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
assert_eq!(out[0].when.0[0].0, vec![le("c", json!("z"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_trigger_matches_superset_conjunction() {
|
||||
let ts = vec![alias_t(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "b".into(),
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
vec![("x", json!("v"))],
|
||||
)];
|
||||
let rules = vec![rule(
|
||||
PredAll {
|
||||
all: vec![
|
||||
LeafEquals {
|
||||
r#ref: "a".into(),
|
||||
equals: json!(1),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "b".into(),
|
||||
equals: json!(2),
|
||||
}
|
||||
.into(),
|
||||
LeafEquals {
|
||||
r#ref: "c".into(),
|
||||
equals: json!(3),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
)];
|
||||
let out = normalize(&rules, ts);
|
||||
let conj = &out[0].when.0[0];
|
||||
assert!(conj.0.contains(&le("c", json!(3))));
|
||||
assert!(conj.0.contains(&le("x", json!("v"))));
|
||||
assert_eq!(conj.0.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::bail;
|
||||
use proc_macro2::{Ident, Literal, TokenStream};
|
||||
use quote::quote;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ast::{
|
||||
Assignment, AssignmentEffect, Conjunction, Dnf, Field, FujiOption, Leaf, Predicate,
|
||||
Setting, Severity, SpecKind, Transformation,
|
||||
},
|
||||
common::options,
|
||||
schema::alias::NormalizedRule,
|
||||
util::ident::safe_upper_camel_case_ident,
|
||||
};
|
||||
|
||||
pub trait OptionLike {
|
||||
fn id(&self) -> &str;
|
||||
fn option_ref(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
impl OptionLike for Setting {
|
||||
fn id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
}
|
||||
|
||||
fn option_ref(&self) -> Option<&str> {
|
||||
Some(&self.r#ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl OptionLike for Field {
|
||||
fn id(&self) -> &str {
|
||||
Field::id(self)
|
||||
}
|
||||
|
||||
fn option_ref(&self) -> Option<&str> {
|
||||
match self {
|
||||
Field::Ref(r) => Some(&r.r#ref),
|
||||
Field::Inline(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum CompareOp {
|
||||
Eq,
|
||||
Lt,
|
||||
Lte,
|
||||
Gt,
|
||||
Gte,
|
||||
}
|
||||
|
||||
impl CompareOp {
|
||||
fn tokens(self) -> TokenStream {
|
||||
match self {
|
||||
Self::Eq => quote! { == },
|
||||
Self::Lt => quote! { < },
|
||||
Self::Lte => quote! { <= },
|
||||
Self::Gt => quote! { > },
|
||||
Self::Gte => quote! { >= },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SettingInfo<'a> {
|
||||
pub id: &'a str,
|
||||
pub kind: SpecKind,
|
||||
pub option: Option<&'a FujiOption>,
|
||||
}
|
||||
|
||||
impl<'a> SettingInfo<'a> {
|
||||
pub fn field_ident(&self) -> Ident {
|
||||
Ident::new(self.id, proc_macro2::Span::call_site())
|
||||
}
|
||||
|
||||
pub fn type_path(&self) -> TokenStream {
|
||||
match self.option {
|
||||
Some(option) => {
|
||||
let options_path = options::path();
|
||||
let type_ident = safe_upper_camel_case_ident(&option.id);
|
||||
quote! { #options_path::#type_ident }
|
||||
}
|
||||
None => {
|
||||
debug_assert!(matches!(self.kind, SpecKind::Integer));
|
||||
quote! { i32 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn int(&self, accessor: &TokenStream) -> TokenStream {
|
||||
if self.option.is_some() {
|
||||
quote! { i32::from(#accessor) }
|
||||
} else {
|
||||
quote! { #accessor }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_settings<'a, F: OptionLike + 'a>(
|
||||
options: &'a BTreeMap<String, FujiOption>,
|
||||
items: &'a [F],
|
||||
) -> anyhow::Result<BTreeMap<&'a str, SettingInfo<'a>>> {
|
||||
let mut table = BTreeMap::new();
|
||||
for item in items {
|
||||
let info = if let Some(name) = item.option_ref() {
|
||||
let option = options.get(name).expect("ref validated during cue export");
|
||||
SettingInfo {
|
||||
id: item.id(),
|
||||
kind: option.spec.kind(),
|
||||
option: Some(option),
|
||||
}
|
||||
} else {
|
||||
SettingInfo {
|
||||
id: item.id(),
|
||||
kind: SpecKind::Integer,
|
||||
option: None,
|
||||
}
|
||||
};
|
||||
let _ = table.insert(info.id, info);
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
pub fn generate_predicate(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
pred: &Predicate,
|
||||
accessor: &TokenStream,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
Ok(match pred {
|
||||
Predicate::Bool(b) => {
|
||||
if *b {
|
||||
quote! { true }
|
||||
} else {
|
||||
quote! { false }
|
||||
}
|
||||
}
|
||||
Predicate::All(p) => {
|
||||
if p.all.is_empty() {
|
||||
quote! { true }
|
||||
} else if p.all.len() == 1 {
|
||||
generate_predicate(settings, &p.all[0], accessor)?
|
||||
} else {
|
||||
let parts = p
|
||||
.all
|
||||
.iter()
|
||||
.map(|c| generate_predicate(settings, c, accessor))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
quote! { #( ( #parts ) )&&* }
|
||||
}
|
||||
}
|
||||
Predicate::Any(p) => {
|
||||
if p.any.is_empty() {
|
||||
quote! { false }
|
||||
} else if p.any.len() == 1 {
|
||||
generate_predicate(settings, &p.any[0], accessor)?
|
||||
} else {
|
||||
let parts = p
|
||||
.any
|
||||
.iter()
|
||||
.map(|c| generate_predicate(settings, c, accessor))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
quote! { #( ( #parts ) )||* }
|
||||
}
|
||||
}
|
||||
Predicate::Not(p) => {
|
||||
let inner = generate_predicate(settings, &p.not, accessor)?;
|
||||
quote! { !( #inner ) }
|
||||
}
|
||||
Predicate::Present(p) => {
|
||||
let info = settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
if p.present {
|
||||
quote! { #accessor.#field.is_some() }
|
||||
} else {
|
||||
quote! { #accessor.#field.is_none() }
|
||||
}
|
||||
}
|
||||
Predicate::Equals(p) => {
|
||||
let info = settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
generate_compare(info, accessor, &p.equals, CompareOp::Eq)?
|
||||
}
|
||||
Predicate::In(p) => {
|
||||
let info = settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
let arms = p
|
||||
.values
|
||||
.iter()
|
||||
.map(|v| generate_value_expr(info, v))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
match info.kind {
|
||||
SpecKind::Enum => {
|
||||
quote! { #accessor.#field.is_some_and(|v| matches!(v, #( #arms )|*)) }
|
||||
}
|
||||
SpecKind::Integer => {
|
||||
let v_expr = info.int("e! { v });
|
||||
quote! { #accessor.#field.is_some_and(|v| matches!(#v_expr, #( #arms )|*)) }
|
||||
}
|
||||
SpecKind::Float => {
|
||||
quote! {
|
||||
#accessor.#field.is_some_and(|v| {
|
||||
let v = f32::from(v);
|
||||
#( (v - (#arms)).abs() < f32::EPSILON )||*
|
||||
})
|
||||
}
|
||||
}
|
||||
SpecKind::String => {
|
||||
quote! { #accessor.#field.as_deref().is_some_and(|v| matches!(v, #( #arms )|*)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Predicate::Between(p) => {
|
||||
let info = settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
let lo = generate_value_expr(info, &p.min)?;
|
||||
let hi = generate_value_expr(info, &p.max)?;
|
||||
match info.kind {
|
||||
SpecKind::Integer => {
|
||||
let v_expr = info.int("e! { v });
|
||||
quote! { #accessor.#field.is_some_and(|v| ( (#lo) ..= (#hi) ).contains(&#v_expr)) }
|
||||
}
|
||||
SpecKind::Float => {
|
||||
quote! { #accessor.#field.is_some_and(|v| ( (#lo) ..= (#hi) ).contains(&f32::from(v))) }
|
||||
}
|
||||
other => bail!(
|
||||
"`between` predicate on non-numeric setting `{}` ({other:?})",
|
||||
p.r#ref
|
||||
),
|
||||
}
|
||||
}
|
||||
Predicate::LessThan(p) => generate_compare(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
&p.lt,
|
||||
CompareOp::Lt,
|
||||
)?,
|
||||
Predicate::LessThanOrEqual(p) => generate_compare(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
&p.lte,
|
||||
CompareOp::Lte,
|
||||
)?,
|
||||
Predicate::GreaterThan(p) => generate_compare(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
&p.gt,
|
||||
CompareOp::Gt,
|
||||
)?,
|
||||
Predicate::GreaterThanOrEqual(p) => generate_compare(
|
||||
settings
|
||||
.get(p.r#ref.as_str())
|
||||
.expect("ref validated during cue export"),
|
||||
accessor,
|
||||
&p.gte,
|
||||
CompareOp::Gte,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_assignment(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
assignment: &Assignment,
|
||||
accessor: &TokenStream,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let info = settings
|
||||
.get(assignment.r#ref.as_str())
|
||||
.expect("ref validated during cue export");
|
||||
let field = info.field_ident();
|
||||
Ok(match &assignment.effect {
|
||||
AssignmentEffect::Set(value) => {
|
||||
let value = generate_value_expr(info, value)?;
|
||||
quote! { #accessor.#field = Some(#value); }
|
||||
}
|
||||
AssignmentEffect::Clear => quote! { #accessor.#field = None; },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_transformation(
|
||||
settings: &BTreeMap<&str, SettingInfo<'_>>,
|
||||
transformation: &Transformation,
|
||||
accessor: &TokenStream,
|
||||
) -> anyhow::Result<TokenStream> {
|
||||
let assignments = transformation
|
||||
.apply
|
||||
.iter()
|
||||
.map(|a| generate_assignment(settings, a, accessor))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
let body = quote! { #( #assignments )* };
|
||||
Ok(match &transformation.when {
|
||||
Some(pred) => {
|
||||