feat: add Fuji Modelling Language codegen
Build and Release Binary / build (push) Successful in 10m13s
Build and Release Binary / release (push) Successful in 7m34s

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2026-05-25 10:42:12 +00:00
parent 0288a20b82
commit edfb8e39b6
131 changed files with 13228 additions and 4006 deletions
+24 -9
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+26 -92
View File
@@ -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)
+33
View File
@@ -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(())
}
+21
View File
@@ -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"] }
+321
View File
@@ -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());
}
}
+605
View File
@@ -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))
);
}
}
+14
View File
@@ -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,
}
+430
View File
@@ -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());
}
}
+98
View File
@@ -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");
}
}
+247
View File
@@ -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");
}
}
+25
View File
@@ -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
})
}
+129
View File
@@ -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()
}
}
}
}
}
+155
View File
@@ -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()
}
+106
View File
@@ -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 }
}
+4
View File
@@ -0,0 +1,4 @@
pub mod cameras;
pub mod options;
pub mod renders;
pub mod simulations;
+152
View File
@@ -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))
}
}
})
}
+69
View File
@@ -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 }
}
})
}
+144
View File
@@ -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 )*
}
}
}
}
+461
View File
@@ -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, &quote! { 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, &quote! { 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,
)
}
}
}
}
+30
View File
@@ -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, &quote! { 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(&current, 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 }
}
+75
View File
@@ -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))
}
+496
View 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);
}
}
+497
View File
@@ -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(&quote! { 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(&quote! { 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) => {