use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
fn deserialize_char_map<'de, D, V>(deserializer: D) -> Result<HashMap<char, V>, D::Error>
where
D: Deserializer<'de>,
V: Deserialize<'de>,
{
let string_map = HashMap::<String, V>::deserialize(deserializer)?;
string_map
.into_iter()
.map(|(k, v)| {
let ch = k
.chars()
.next()
.ok_or_else(|| serde::de::Error::custom("empty key in trie"))?;
if k.len() != ch.len_utf8() {
return Err(serde::de::Error::custom(format!(
"multi-char key in trie: {:?}",
k
)));
}
Ok((ch, v))
})
.collect()
}
fn serialize_char_map<S, V: Serialize>(
map: &HashMap<char, V>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let mut ser_map = serializer.serialize_map(Some(map.len()))?;
for (k, v) in map {
ser_map.serialize_entry(&k.to_string(), v)?;
}
ser_map.end()
}
pub const TABLE_FORMAT_VERSION: &str = "0.3.0";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CommandScope {
Document,
Field,
Job,
Session,
Label,
}
impl std::fmt::Display for CommandScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CommandScope::Document => write!(f, "document"),
CommandScope::Field => write!(f, "field"),
CommandScope::Job => write!(f, "job"),
CommandScope::Session => write!(f, "session"),
CommandScope::Label => write!(f, "label"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CommandCategory {
Text,
Barcode,
Graphics,
Media,
Format,
Device,
Host,
Config,
Network,
Rfid,
Wireless,
Storage,
Kdu,
Misc,
}
impl std::fmt::Display for CommandCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CommandCategory::Text => write!(f, "text"),
CommandCategory::Barcode => write!(f, "barcode"),
CommandCategory::Graphics => write!(f, "graphics"),
CommandCategory::Media => write!(f, "media"),
CommandCategory::Format => write!(f, "format"),
CommandCategory::Device => write!(f, "device"),
CommandCategory::Host => write!(f, "host"),
CommandCategory::Config => write!(f, "config"),
CommandCategory::Network => write!(f, "network"),
CommandCategory::Rfid => write!(f, "rfid"),
CommandCategory::Wireless => write!(f, "wireless"),
CommandCategory::Storage => write!(f, "storage"),
CommandCategory::Kdu => write!(f, "kdu"),
CommandCategory::Misc => write!(f, "misc"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Stability {
Stable,
Experimental,
Deprecated,
}
impl std::fmt::Display for Stability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Stability::Stable => write!(f, "stable"),
Stability::Experimental => write!(f, "experimental"),
Stability::Deprecated => write!(f, "deprecated"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParserTables {
pub schema_version: String,
#[serde(default = "default_format_version")]
pub format_version: String,
pub commands: Vec<CommandEntry>,
#[serde(default)]
pub opcode_trie: Option<OpcodeTrieNode>,
#[serde(skip)]
code_set_cache: OnceLock<HashSet<String>>,
#[serde(skip)]
cmd_map: OnceLock<HashMap<String, usize>>,
}
fn default_format_version() -> String {
TABLE_FORMAT_VERSION.to_string()
}
impl ParserTables {
pub fn new(
schema_version: String,
format_version: String,
commands: Vec<CommandEntry>,
opcode_trie: Option<OpcodeTrieNode>,
) -> Self {
Self {
schema_version,
format_version,
commands,
opcode_trie,
code_set_cache: OnceLock::new(),
cmd_map: OnceLock::new(),
}
}
pub fn code_set(&self) -> &HashSet<String> {
self.code_set_cache.get_or_init(|| {
self.commands
.iter()
.flat_map(|c| c.codes.iter().cloned())
.collect()
})
}
fn cmd_map(&self) -> &HashMap<String, usize> {
self.cmd_map.get_or_init(|| {
let mut m = HashMap::new();
for (i, c) in self.commands.iter().enumerate() {
for code in &c.codes {
m.insert(code.clone(), i);
}
}
m
})
}
pub fn cmd_by_code(&self, code: &str) -> Option<&CommandEntry> {
self.cmd_map().get(code).map(|&i| &self.commands[i])
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandEntry {
pub codes: Vec<String>,
pub arity: u32,
#[serde(default)]
pub raw_payload: bool,
#[serde(default)]
pub field_data: bool,
#[serde(default)]
pub opens_field: bool,
#[serde(default)]
pub closes_field: bool,
#[serde(default)]
pub hex_escape_modifier: bool,
#[serde(default)]
pub field_number: bool,
#[serde(default)]
pub serialization: bool,
#[serde(default)]
pub requires_field: bool,
#[serde(default)]
pub signature: Option<Signature>,
#[serde(default)]
pub args: Option<Vec<ArgUnion>>,
#[serde(default)]
pub constraints: Option<Vec<Constraint>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constraint_defaults: Option<ConstraintDefaults>,
#[serde(default)]
pub effects: Option<Effects>,
#[serde(default)]
pub plane: Option<Plane>,
#[serde(default)]
pub scope: Option<CommandScope>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placement: Option<Placement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<CommandCategory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated_since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stability: Option<Stability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub composites: Option<Vec<Composite>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub defaults: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub units: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub printer_gates: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature_overrides: Option<HashMap<String, Signature>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field_data_rules: Option<FieldDataRules>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<Example>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Composite {
pub name: String,
pub template: String,
pub exposes_args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Effects {
#[serde(default)]
pub sets: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Placement {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_inside_label: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_outside_label: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FieldDataRules {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub character_set: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub character_set_severity: Option<ConstraintSeverity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exact_length: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_lengths: Option<Vec<usize>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub length_parity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub length_severity: Option<ConstraintSeverity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SplitRule {
pub param_index: usize,
pub char_counts: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Signature {
pub params: Vec<String>,
#[serde(default = "default_joiner")]
pub joiner: String,
#[serde(default = "default_no_space_after_opcode")]
pub no_space_after_opcode: bool,
#[serde(default = "default_allow_empty_trailing")]
pub allow_empty_trailing: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub split_rule: Option<SplitRule>,
}
fn default_joiner() -> String {
",".to_string()
}
fn default_no_space_after_opcode() -> bool {
true
}
fn default_allow_empty_trailing() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpcodeTrieNode {
#[serde(
default,
deserialize_with = "deserialize_char_map",
serialize_with = "serialize_char_map"
)]
pub children: HashMap<char, OpcodeTrieNode>,
#[serde(default)]
pub terminal: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ArgUnion {
Single(Box<Arg>),
OneOf {
#[serde(rename = "oneOf")]
one_of: Vec<Arg>,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ArgPresence {
#[serde(rename = "unset")]
Unset,
#[serde(rename = "empty")]
Empty,
#[serde(rename = "value")]
Value,
#[serde(rename = "valueOrDefault")]
ValueOrDefault,
#[serde(rename = "emptyMeansUseDefault")]
EmptyMeansUseDefault,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ResourceKind {
Graphic,
Font,
Any,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Arg {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub key: Option<String>,
#[serde(rename = "type")]
pub r#type: String,
#[serde(default)]
pub unit: Option<String>,
#[serde(default)]
pub range: Option<[f64; 2]>,
#[serde(default)]
pub min_length: Option<u32>,
#[serde(default)]
pub max_length: Option<u32>,
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub presence: Option<ArgPresence>,
#[serde(default)]
pub default: Option<serde_json::Value>,
#[serde(default)]
pub default_by_dpi: Option<std::collections::HashMap<String, serde_json::Value>>,
#[serde(default)]
pub default_from: Option<String>,
#[serde(default)]
pub default_from_state_key: Option<String>,
#[serde(default)]
pub profile_constraint: Option<ProfileConstraint>,
#[serde(default)]
pub range_when: Option<Vec<ConditionalRange>>,
#[serde(default)]
pub rounding_policy: Option<RoundingPolicy>,
#[serde(default)]
pub rounding_policy_when: Option<Vec<ConditionalRounding>>,
#[serde(default)]
pub resource: Option<ResourceKind>,
#[serde(default)]
pub r#enum: Option<Vec<EnumValue>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ComparisonOp {
Lte,
Gte,
Lt,
Gt,
Eq,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileConstraint {
pub field: String,
pub op: ComparisonOp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EnumValue {
Simple(String),
Object {
value: String,
#[serde(default, rename = "printerGates")]
printer_gates: Option<Vec<String>>,
#[serde(default)]
extras: Option<serde_json::Value>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionalRange {
pub when: String,
pub range: [f64; 2],
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum RoundingMode {
ToMultiple,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoundingPolicy {
#[serde(default)]
pub unit: Option<String>,
pub mode: RoundingMode,
#[serde(default)]
pub multiple: Option<f64>,
#[serde(default = "default_rounding_epsilon")]
pub epsilon: f64,
}
fn default_rounding_epsilon() -> f64 {
1e-9
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionalRounding {
pub when: String,
pub mode: RoundingMode,
#[serde(default)]
pub multiple: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub epsilon: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConstraintDefaults {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<ConstraintSeverity>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ConstraintKind {
Order,
Requires,
Incompatible,
EmptyData,
Range,
Note,
Custom,
}
impl ConstraintKind {
pub const ALL: &[Self] = &[
Self::Order,
Self::Requires,
Self::Incompatible,
Self::EmptyData,
Self::Range,
Self::Note,
Self::Custom,
];
}
impl std::fmt::Display for ConstraintKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConstraintKind::Order => write!(f, "order"),
ConstraintKind::Requires => write!(f, "requires"),
ConstraintKind::Incompatible => write!(f, "incompatible"),
ConstraintKind::EmptyData => write!(f, "emptyData"),
ConstraintKind::Range => write!(f, "range"),
ConstraintKind::Note => write!(f, "note"),
ConstraintKind::Custom => write!(f, "custom"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConstraintSeverity {
Error,
Warn,
Info,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum NoteAudience {
Problem,
Contextual,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConstraintScope {
Label,
Field,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Plane {
Format,
Device,
Host,
Config,
}
impl std::fmt::Display for Plane {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Plane::Format => write!(f, "format"),
Plane::Device => write!(f, "device"),
Plane::Host => write!(f, "host"),
Plane::Config => write!(f, "config"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Example {
pub zpl: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub png_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profiles: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Constraint {
pub kind: ConstraintKind,
#[serde(default)]
pub expr: Option<String>,
pub message: String,
#[serde(default)]
pub severity: Option<ConstraintSeverity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<ConstraintScope>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<NoteAudience>,
}
#[cfg(test)]
mod tests {
use super::{
Arg, ArgPresence, ConstraintDefaults, ConstraintSeverity, ResourceKind, RoundingPolicy,
Signature,
};
#[test]
fn signature_allow_empty_trailing_defaults_true() {
let sig: Signature =
serde_json::from_str(r#"{"params":["a"],"joiner":","}"#).expect("valid signature");
assert!(
sig.allow_empty_trailing,
"allow_empty_trailing should default to true to match schema"
);
}
#[test]
fn arg_presence_and_resource_deserialize() {
let arg: Arg = serde_json::from_str(
r#"{
"name":"obj",
"type":"resourceRef",
"presence":"valueOrDefault",
"resource":"font"
}"#,
)
.expect("valid arg");
assert_eq!(arg.presence, Some(ArgPresence::ValueOrDefault));
assert_eq!(arg.resource, Some(ResourceKind::Font));
}
#[test]
fn rounding_policy_epsilon_defaults_to_small_tolerance() {
let policy: RoundingPolicy =
serde_json::from_str(r#"{"mode":"toMultiple","multiple":2}"#).expect("valid policy");
assert!(
(policy.epsilon - 1e-9).abs() < f64::EPSILON,
"rounding epsilon should default to 1e-9"
);
}
#[test]
fn constraint_defaults_deserialize() {
let defaults: ConstraintDefaults =
serde_json::from_str(r#"{"severity":"error"}"#).expect("valid constraint defaults");
assert_eq!(defaults.severity, Some(ConstraintSeverity::Error));
}
}