use super::types::{
CapabilityValue, EventManifest, EventPayloadShape, PropertySchema, PropertyValueKind,
};
use crate::compat::{format, String, Vec};
mod keys {
pub(crate) const CONTROL: &str = "control";
pub(crate) const NAME: &str = "name";
pub(crate) const ALIASES: &str = "aliases";
pub(crate) const PROPERTIES: &str = "properties";
pub(crate) const EVENTS: &str = "events";
pub(crate) const COMMANDS: &str = "commands";
pub(crate) const KIND: &str = "kind";
pub(crate) const READABLE: &str = "readable";
pub(crate) const WRITABLE: &str = "writable";
pub(crate) const TOKENS: &str = "tokens";
pub(crate) const DEFAULT: &str = "default";
pub(crate) const PAYLOAD: &str = "payload";
pub(crate) const SHAPE: &str = "shape";
}
pub fn value_kind_token(kind: PropertyValueKind) -> &'static str {
match kind {
PropertyValueKind::Bool => "bool",
PropertyValueKind::Int => "int",
PropertyValueKind::UInt => "uint",
PropertyValueKind::Float => "float",
PropertyValueKind::Number => "number",
PropertyValueKind::String => "string",
PropertyValueKind::Enum => "enum",
PropertyValueKind::Color => "color",
PropertyValueKind::Rect => "rect",
}
}
pub fn value_kind_from_token(token: &str) -> Option<PropertyValueKind> {
match token {
"bool" => Some(PropertyValueKind::Bool),
"int" => Some(PropertyValueKind::Int),
"uint" => Some(PropertyValueKind::UInt),
"float" => Some(PropertyValueKind::Float),
"number" => Some(PropertyValueKind::Number),
"string" => Some(PropertyValueKind::String),
"enum" => Some(PropertyValueKind::Enum),
"color" => Some(PropertyValueKind::Color),
"rect" => Some(PropertyValueKind::Rect),
_ => None,
}
}
pub fn shape_token(shape: EventPayloadShape) -> &'static str {
match shape {
EventPayloadShape::Scalar => "scalar",
EventPayloadShape::OptionalScalar => "optional",
EventPayloadShape::ListScalar => "list",
EventPayloadShape::Tuple2 => "tuple2",
EventPayloadShape::Tuple3 => "tuple3",
EventPayloadShape::Tuple4 => "tuple4",
EventPayloadShape::OptionalTuple2 => "optional_tuple2",
EventPayloadShape::Mixed => "mixed",
}
}
pub fn shape_from_token(token: &str) -> Option<EventPayloadShape> {
match token {
"scalar" => Some(EventPayloadShape::Scalar),
"optional" => Some(EventPayloadShape::OptionalScalar),
"list" => Some(EventPayloadShape::ListScalar),
"tuple2" => Some(EventPayloadShape::Tuple2),
"tuple3" => Some(EventPayloadShape::Tuple3),
"tuple4" => Some(EventPayloadShape::Tuple4),
"optional_tuple2" => Some(EventPayloadShape::OptionalTuple2),
"mixed" => Some(EventPayloadShape::Mixed),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DesignerProperty {
pub name: String,
pub kind: PropertyValueKind,
pub readable: bool,
pub writable: bool,
pub tokens: Vec<String>,
pub default_value: CapabilityValue,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DesignerEvent {
pub name: String,
pub payload: Option<PropertyValueKind>,
pub shape: Option<EventPayloadShape>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DesignerManifest {
pub control: String,
pub aliases: Vec<String>,
pub properties: Vec<DesignerProperty>,
pub events: Vec<DesignerEvent>,
pub commands: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManifestParseError {
pub detail: String,
}
impl core::fmt::Display for ManifestParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.detail)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManifestExportError {
UnknownControl,
IncompleteDefaults,
}
impl core::fmt::Display for ManifestExportError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownControl => f.write_str("no control registered under that name"),
Self::IncompleteDefaults => f.write_str(
"a property default value could not be read, so the export is incomplete",
),
}
}
}
fn push_json_string(out: &mut String, text: &str) {
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 || c as u32 == 0x7f => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
fn push_json_value(out: &mut String, value: &CapabilityValue) {
match value {
CapabilityValue::Null => out.push_str("null"),
CapabilityValue::Bool(inner) => out.push_str(if *inner { "true" } else { "false" }),
CapabilityValue::Int(inner) => out.push_str(&format!("{inner}")),
CapabilityValue::UInt(inner) => out.push_str(&format!("{inner}")),
CapabilityValue::Float(inner) => push_json_float(out, *inner),
CapabilityValue::String(inner) => push_json_string(out, inner),
CapabilityValue::Color(color) => {
out.push_str(&format!(
"\"#{:02x}{:02x}{:02x}{:02x}\"",
color.r, color.g, color.b, color.a
));
}
CapabilityValue::Rect(rect) => {
out.push_str(&format!("\"{},{},{},{}\"", rect.x, rect.y, rect.width, rect.height));
}
}
}
fn push_json_float(out: &mut String, value: f64) {
if !value.is_finite() {
out.push_str("null");
return;
}
let rendered = format!("{value}");
if rendered.contains('.') || rendered.contains('e') || rendered.contains('E') {
out.push_str(&rendered);
return;
}
out.push_str(&rendered);
out.push_str(".0");
}
fn write_manifest(out: &mut String, manifest: &DesignerManifest) {
out.push_str("{\n");
out.push_str(" ");
push_json_string(out, keys::CONTROL);
out.push_str(": ");
push_json_string(out, &manifest.control);
out.push_str(",\n");
push_array(
out,
keys::ALIASES,
&manifest.aliases,
|out, alias| {
push_json_string(out, alias);
},
true,
);
out.push_str(" ");
push_json_string(out, keys::PROPERTIES);
out.push_str(": [\n");
for (index, property) in manifest.properties.iter().enumerate() {
out.push_str(" {\n ");
push_json_string(out, keys::NAME);
out.push_str(": ");
push_json_string(out, &property.name);
out.push_str(",\n ");
push_json_string(out, keys::KIND);
out.push_str(": ");
push_json_string(out, value_kind_token(property.kind));
out.push_str(",\n ");
push_json_string(out, keys::READABLE);
out.push_str(&format!(": {},\n ", property.readable));
push_json_string(out, keys::WRITABLE);
out.push_str(&format!(": {},\n ", property.writable));
push_json_string(out, keys::TOKENS);
out.push_str(": ");
push_token_array(out, &property.tokens);
out.push_str(",\n ");
push_json_string(out, keys::DEFAULT);
out.push_str(": ");
push_json_value(out, &property.default_value);
out.push_str("\n }");
out.push_str(if index + 1 == manifest.properties.len() { "\n" } else { ",\n" });
}
out.push_str(" ],\n");
out.push_str(" ");
push_json_string(out, keys::EVENTS);
out.push_str(": [\n");
for (index, event) in manifest.events.iter().enumerate() {
out.push_str(" {\n ");
push_json_string(out, keys::NAME);
out.push_str(": ");
push_json_string(out, &event.name);
out.push_str(",\n ");
push_json_string(out, keys::PAYLOAD);
out.push_str(": ");
match event.payload {
Some(kind) => push_json_string(out, value_kind_token(kind)),
None => out.push_str("null"),
}
out.push_str(",\n ");
push_json_string(out, keys::SHAPE);
out.push_str(": ");
match event.shape {
Some(shape) => push_json_string(out, shape_token(shape)),
None => out.push_str("null"),
}
out.push_str("\n }");
out.push_str(if index + 1 == manifest.events.len() { "\n" } else { ",\n" });
}
out.push_str(" ],\n");
push_array(
out,
keys::COMMANDS,
&manifest.commands,
|out, command| {
push_json_string(out, command);
},
false,
);
out.push_str("}\n");
}
fn push_array<T, F>(out: &mut String, key: &str, items: &[T], mut write: F, trailing_comma: bool)
where
F: FnMut(&mut String, &T),
{
let sep = if trailing_comma { ",\n" } else { "\n" };
out.push_str(" ");
push_json_string(out, key);
out.push_str(": [");
if items.is_empty() {
out.push(']');
out.push_str(sep);
return;
}
out.push('\n');
for (index, item) in items.iter().enumerate() {
out.push_str(" ");
write(out, item);
out.push_str(if index + 1 == items.len() { "\n" } else { ",\n" });
}
out.push_str(" ]");
out.push_str(sep);
}
fn push_token_array(out: &mut String, items: &[String]) {
out.push('[');
if items.is_empty() {
out.push(']');
return;
}
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
push_json_string(out, item);
}
out.push(']');
}
struct JsonReader<'a> {
bytes: &'a [u8],
cursor: usize,
}
impl<'a> JsonReader<'a> {
fn new(text: &'a str) -> Self {
Self { bytes: text.as_bytes(), cursor: 0 }
}
fn fail<T>(&self, detail: &str) -> Result<T, ManifestParseError> {
Err(ManifestParseError { detail: format!("{} at byte {}", detail, self.cursor) })
}
fn skip_whitespace(&mut self) {
while let Some(byte) = self.bytes.get(self.cursor) {
if byte.is_ascii_whitespace() {
self.cursor += 1;
} else {
break;
}
}
}
fn expect(&mut self, expected: u8, what: &str) -> Result<(), ManifestParseError> {
self.skip_whitespace();
match self.bytes.get(self.cursor) {
Some(byte) if *byte == expected => {
self.cursor += 1;
Ok(())
}
_ => self.fail(what),
}
}
fn peek(&mut self, byte: u8) -> bool {
self.skip_whitespace();
self.bytes.get(self.cursor) == Some(&byte)
}
fn read_null(&mut self) -> Result<(), ManifestParseError> {
self.skip_whitespace();
if self.bytes[self.cursor..].starts_with(b"null") {
self.cursor += 4;
return Ok(());
}
self.fail("expected `null`")
}
fn read_bool(&mut self) -> Result<bool, ManifestParseError> {
self.skip_whitespace();
if self.bytes[self.cursor..].starts_with(b"true") {
self.cursor += 4;
return Ok(true);
}
if self.bytes[self.cursor..].starts_with(b"false") {
self.cursor += 5;
return Ok(false);
}
self.fail("expected `true` or `false`")
}
fn read_string(&mut self) -> Result<String, ManifestParseError> {
self.expect(b'"', "expected a string")?;
let mut out = String::new();
loop {
let Some(byte) = self.bytes.get(self.cursor).copied() else {
return self.fail("unterminated string");
};
self.cursor += 1;
match byte {
b'"' => return Ok(out),
b'\\' => {
let Some(escape) = self.bytes.get(self.cursor).copied() else {
return self.fail("unterminated escape");
};
self.cursor += 1;
match escape {
b'"' => out.push('"'),
b'\\' => out.push('\\'),
b'n' => out.push('\n'),
b'r' => out.push('\r'),
b't' => out.push('\t'),
b'u' => {
let code = self.read_hex4()?;
let Some(ch) = char::from_u32(code) else {
return self.fail("invalid \\u escape");
};
out.push(ch);
}
_ => return self.fail("unknown escape"),
}
}
_ => {
let start = self.cursor - 1;
let width = utf8_width(byte);
if width == 1 {
out.push(byte as char);
} else {
let end = start + width;
if end > self.bytes.len() {
return self.fail("truncated UTF-8 sequence");
}
let Some(text) = core::str::from_utf8(&self.bytes[start..end]).ok() else {
return self.fail("invalid UTF-8 sequence");
};
out.push_str(text);
self.cursor = end;
}
}
}
}
}
fn read_hex4(&mut self) -> Result<u32, ManifestParseError> {
let mut value = 0u32;
for _ in 0..4 {
let Some(byte) = self.bytes.get(self.cursor).copied() else {
return self.fail("truncated \\u escape");
};
self.cursor += 1;
let digit = match byte {
b'0'..=b'9' => (byte - b'0') as u32,
b'a'..=b'f' => (byte - b'a') as u32 + 10,
b'A'..=b'F' => (byte - b'A') as u32 + 10,
_ => return self.fail("non-hex digit in \\u escape"),
};
value = value * 16 + digit;
}
Ok(value)
}
fn read_number(&mut self) -> Result<(f64, bool), ManifestParseError> {
self.skip_whitespace();
let start = self.cursor;
while let Some(byte) = self.bytes.get(self.cursor) {
if byte.is_ascii_digit() || matches!(byte, b'-' | b'+' | b'.' | b'e' | b'E') {
self.cursor += 1;
} else {
break;
}
}
if start == self.cursor {
return self.fail("expected a number");
}
let Some(text) = core::str::from_utf8(&self.bytes[start..self.cursor]).ok() else {
return self.fail("number is not ASCII");
};
let is_float = text.contains('.') || text.contains('e') || text.contains('E');
match text.parse::<f64>() {
Ok(value) => Ok((value, is_float)),
Err(_) => self.fail("malformed number"),
}
}
fn read_key(&mut self) -> Result<String, ManifestParseError> {
let key = self.read_string()?;
self.expect(b':', "expected `:` after a key")?;
Ok(key)
}
}
fn utf8_width(first: u8) -> usize {
match first {
0x00..=0x7f => 1,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
_ => 4,
}
}
fn read_string_array(reader: &mut JsonReader<'_>) -> Result<Vec<String>, ManifestParseError> {
reader.expect(b'[', "expected `[`")?;
let mut items = Vec::new();
if reader.peek(b']') {
reader.expect(b']', "expected `]`")?;
return Ok(items);
}
loop {
items.push(reader.read_string()?);
if reader.peek(b',') {
reader.expect(b',', "expected `,`")?;
continue;
}
reader.expect(b']', "expected `]` or `,` in a string array")?;
return Ok(items);
}
}
fn read_property(reader: &mut JsonReader<'_>) -> Result<DesignerProperty, ManifestParseError> {
reader.expect(b'{', "expected `{` to start a property")?;
let mut name = None;
let mut kind = None;
let mut readable = None;
let mut writable = None;
let mut tokens = Vec::new();
let mut default_value = CapabilityValue::Null;
let mut saw_default = false;
loop {
let key = reader.read_key()?;
match key.as_str() {
k if k == keys::NAME => name = Some(reader.read_string()?),
k if k == keys::KIND => {
let token = reader.read_string()?;
kind = value_kind_from_token(&token);
if kind.is_none() {
return Err(ManifestParseError {
detail: format!("unknown property kind `{token}`"),
});
}
}
k if k == keys::READABLE => readable = Some(reader.read_bool()?),
k if k == keys::WRITABLE => writable = Some(reader.read_bool()?),
k if k == keys::TOKENS => tokens = read_string_array(reader)?,
k if k == keys::DEFAULT => {
default_value = read_value(reader)?;
saw_default = true;
}
other => {
return Err(ManifestParseError {
detail: format!("unexpected key `{other}` in a property"),
})
}
}
if reader.peek(b',') {
reader.expect(b',', "expected `,`")?;
continue;
}
reader.expect(b'}', "expected `}` or `,` in a property")?;
break;
}
Ok(DesignerProperty {
name: name.ok_or(ManifestParseError { detail: "a property has no name".into() })?,
kind: kind.ok_or(ManifestParseError { detail: "a property has no kind".into() })?,
readable: readable.unwrap_or(false),
writable: writable.unwrap_or(false),
tokens,
default_value: if saw_default { default_value } else { CapabilityValue::Null },
})
}
fn read_event(reader: &mut JsonReader<'_>) -> Result<DesignerEvent, ManifestParseError> {
reader.expect(b'{', "expected `{` to start an event")?;
let mut name = None;
let mut payload = None;
let mut shape = None;
loop {
let key = reader.read_key()?;
match key.as_str() {
k if k == keys::NAME => name = Some(reader.read_string()?),
k if k == keys::PAYLOAD => {
if reader.peek(b'n') {
reader.read_null()?;
} else {
let token = reader.read_string()?;
payload = Some(value_kind_from_token(&token).ok_or(ManifestParseError {
detail: format!("unknown event payload kind `{token}`"),
})?);
}
}
k if k == keys::SHAPE => {
if reader.peek(b'n') {
reader.read_null()?;
} else {
let token = reader.read_string()?;
shape = Some(shape_from_token(&token).ok_or(ManifestParseError {
detail: format!("unknown event shape `{token}`"),
})?);
}
}
other => {
return Err(ManifestParseError {
detail: format!("unexpected key `{other}` in an event"),
})
}
}
if reader.peek(b',') {
reader.expect(b',', "expected `,`")?;
continue;
}
reader.expect(b'}', "expected `}` or `,` in an event")?;
break;
}
Ok(DesignerEvent {
name: name.ok_or(ManifestParseError { detail: "an event has no name".into() })?,
payload,
shape,
})
}
fn read_value(reader: &mut JsonReader<'_>) -> Result<CapabilityValue, ManifestParseError> {
if reader.peek(b'n') {
reader.read_null()?;
return Ok(CapabilityValue::Null);
}
if reader.peek(b't') || reader.peek(b'f') {
return Ok(CapabilityValue::Bool(reader.read_bool()?));
}
if reader.peek(b'"') {
return Ok(CapabilityValue::String(reader.read_string()?));
}
let (number, is_float) = reader.read_number()?;
if is_float {
return Ok(CapabilityValue::Float(number));
}
if number < 0.0 {
return Ok(CapabilityValue::Int(number as i64));
}
Ok(CapabilityValue::UInt(number as u64))
}
impl DesignerManifest {
pub fn from_json(text: &str) -> Result<Self, ManifestParseError> {
let mut reader = JsonReader::new(text);
reader.expect(b'{', "expected `{` to start a document")?;
let mut control = None;
let mut aliases = Vec::new();
let mut properties = Vec::new();
let mut events = Vec::new();
let mut commands = Vec::new();
loop {
let key = reader.read_key()?;
match key.as_str() {
k if k == keys::CONTROL => control = Some(reader.read_string()?),
k if k == keys::ALIASES => aliases = read_string_array(&mut reader)?,
k if k == keys::COMMANDS => commands = read_string_array(&mut reader)?,
k if k == keys::PROPERTIES => {
reader.expect(b'[', "expected `[` to start the property list")?;
if !reader.peek(b']') {
loop {
properties.push(read_property(&mut reader)?);
if reader.peek(b',') {
reader.expect(b',', "expected `,`")?;
continue;
}
break;
}
}
reader.expect(b']', "expected `]` after the property list")?;
}
k if k == keys::EVENTS => {
reader.expect(b'[', "expected `[` to start the event list")?;
if !reader.peek(b']') {
loop {
events.push(read_event(&mut reader)?);
if reader.peek(b',') {
reader.expect(b',', "expected `,`")?;
continue;
}
break;
}
}
reader.expect(b']', "expected `]` after the event list")?;
}
other => {
return Err(ManifestParseError {
detail: format!("unexpected key `{other}` in a manifest"),
})
}
}
if reader.peek(b',') {
reader.expect(b',', "expected `,`")?;
continue;
}
reader.expect(b'}', "expected `}` or `,` in a manifest")?;
break;
}
Ok(Self {
control: control.ok_or(ManifestParseError {
detail: "the document does not name its control".into(),
})?,
aliases,
properties,
events,
commands,
})
}
}
#[cfg(full_widgets)]
pub fn designer_manifest(
factory: &super::WidgetFactory,
control: &str,
) -> Result<DesignerManifest, ManifestExportError> {
let capability = factory.capability(control).ok_or(ManifestExportError::UnknownControl)?;
let mut properties = Vec::with_capacity(capability.properties.len());
for schema in capability.properties {
let default_value = factory
.schema_default_value(capability.kind, schema.name)
.ok_or(ManifestExportError::IncompleteDefaults)?;
properties.push(DesignerProperty {
name: schema.name.into(),
kind: schema.value_kind,
readable: schema.readable,
writable: schema.writable,
tokens: schema.accepted_tokens.iter().map(|token| String::from(*token)).collect(),
default_value,
});
}
properties.sort_by(|left, right| left.name.cmp(&right.name));
let mut events: Vec<DesignerEvent> = capability
.events
.iter()
.map(|schema| DesignerEvent {
name: schema.name.into(),
payload: schema.payload,
shape: schema.shape,
})
.collect();
events.sort_by(|left, right| left.name.cmp(&right.name));
let mut commands: Vec<String> =
capability.commands.iter().map(|command| String::from(*command)).collect();
commands.sort();
let mut aliases: Vec<String> =
capability.aliases.iter().map(|alias| String::from(*alias)).collect();
aliases.sort();
Ok(DesignerManifest {
control: capability.canonical_name.into(),
aliases,
properties,
events,
commands,
})
}
#[cfg(full_widgets)]
pub fn capability_manifest_json(
factory: &super::WidgetFactory,
control: &str,
) -> Result<String, ManifestExportError> {
let manifest = designer_manifest(factory, control)?;
let mut out = String::new();
write_manifest(&mut out, &manifest);
Ok(out)
}
pub fn manifest_to_json(manifest: &DesignerManifest) -> String {
let mut out = String::new();
write_manifest(&mut out, manifest);
out
}
#[cfg(full_widgets)]
pub fn all_capability_manifests_json(
factory: &super::WidgetFactory,
) -> Result<String, ManifestExportError> {
let mut names: Vec<&str> =
factory.capabilities().iter().map(|capability| capability.canonical_name).collect();
names.sort_unstable();
let mut out = String::from("[\n");
for (index, name) in names.iter().enumerate() {
let manifest = designer_manifest(factory, name)?;
let mut body = String::new();
write_manifest(&mut body, &manifest);
for line in body.lines() {
if line.is_empty() {
continue;
}
out.push_str(" ");
out.push_str(line);
out.push('\n');
}
if index + 1 != names.len() {
out.push_str(" ,\n");
}
}
out.push(']');
out.push('\n');
Ok(out)
}
pub fn events_of_manifest(manifest: &DesignerManifest) -> &[DesignerEvent] {
&manifest.events
}
impl From<&EventManifest> for DesignerEvent {
fn from(manifest: &EventManifest) -> Self {
Self { name: manifest.name.clone(), payload: manifest.payload, shape: manifest.shape }
}
}
impl From<&PropertySchema> for (String, PropertyValueKind) {
fn from(schema: &PropertySchema) -> Self {
(schema.name.into(), schema.value_kind)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Color, Rect};
fn sample() -> DesignerManifest {
DesignerManifest {
control: String::from("slider"),
aliases: vec![String::from("range")],
properties: vec![DesignerProperty {
name: String::from("value"),
kind: PropertyValueKind::Int,
readable: true,
writable: true,
tokens: Vec::new(),
default_value: CapabilityValue::Int(42),
}],
events: vec![DesignerEvent {
name: String::from("value_changed"),
payload: Some(PropertyValueKind::Int),
shape: Some(EventPayloadShape::Scalar),
}],
commands: vec![String::from("set_value")],
}
}
#[test]
fn a_manifest_round_trips_through_json() {
let manifest = sample();
let first = manifest_to_json(&manifest);
let reloaded = DesignerManifest::from_json(&first).expect("the document this wrote parses");
let second = manifest_to_json(&reloaded);
assert_eq!(first, second, "export -> load -> export changed the document");
}
#[test]
fn a_payload_free_event_round_trips_as_null() {
let mut manifest = sample();
manifest.events.push(DesignerEvent {
name: String::from("slider_pressed"),
payload: None,
shape: None,
});
let json = manifest_to_json(&manifest);
assert!(json.contains("\"payload\": null"), "a payload-free event must declare null");
let reloaded = DesignerManifest::from_json(&json).expect("parses");
assert_eq!(reloaded.events[1].payload, None);
assert_eq!(reloaded.events[1].shape, None);
}
#[test]
fn every_value_variant_round_trips() {
let cases: [(CapabilityValue, CapabilityValue); 9] = [
(CapabilityValue::Null, CapabilityValue::Null),
(CapabilityValue::Bool(true), CapabilityValue::Bool(true)),
(CapabilityValue::Bool(false), CapabilityValue::Bool(false)),
(CapabilityValue::UInt(7), CapabilityValue::UInt(7)),
(CapabilityValue::Float(1.5), CapabilityValue::Float(1.5)),
(CapabilityValue::Int(-3), CapabilityValue::Int(-3)),
(
CapabilityValue::String(String::from("a \"quoted\" \\ value\nwith a newline")),
CapabilityValue::String(String::from("a \"quoted\" \\ value\nwith a newline")),
),
(
CapabilityValue::String(String::from("标签值")),
CapabilityValue::String(String::from("标签值")),
),
(
CapabilityValue::Color(Color::rgba(1, 2, 3, 4)),
CapabilityValue::String(String::from("#01020304")),
),
];
for (written, expected) in cases {
let mut manifest = sample();
manifest.properties[0].default_value = written.clone();
let json = manifest_to_json(&manifest);
let reloaded = DesignerManifest::from_json(&json)
.unwrap_or_else(|error| panic!("{written:?} failed to parse: {error}"));
assert_eq!(
reloaded.properties[0].default_value, expected,
"{written:?} did not survive the round trip"
);
}
}
#[test]
fn structured_values_use_the_property_api_spelling() {
let mut manifest = sample();
manifest.properties[0].default_value = CapabilityValue::Rect(Rect::new(-5, 6, 7, 8));
let json = manifest_to_json(&manifest);
assert!(
json.contains("\"-5,6,7,8\""),
"a rectangle must be written as the `x,y,w,h` string the property API accepts: {json}"
);
manifest.properties[0].default_value =
CapabilityValue::Color(Color::rgba(0xdc, 0xdc, 0xdc, 0xff));
let json = manifest_to_json(&manifest);
assert!(json.contains("\"#dcdcdcff\""), "a colour must be written as `#rrggbbaa`: {json}");
}
#[test]
fn a_whole_float_keeps_its_fractional_part() {
let mut manifest = sample();
manifest.properties[0].default_value = CapabilityValue::Float(1.0);
let json = manifest_to_json(&manifest);
assert!(json.contains("1.0"), "a float must be written so it reads back as a float");
let reloaded = DesignerManifest::from_json(&json).expect("parses");
assert_eq!(reloaded.properties[0].default_value, CapabilityValue::Float(1.0));
}
#[test]
fn an_unrecognised_kind_is_refused() {
let json =
manifest_to_json(&sample()).replace("\"kind\": \"int\"", "\"kind\": \"quaternion\"");
assert_ne!(json, manifest_to_json(&sample()), "the sentinel must actually appear");
let result = DesignerManifest::from_json(&json);
assert!(result.is_err(), "a kind this reader does not know must not be accepted");
}
#[test]
fn an_unknown_key_is_refused() {
let json = manifest_to_json(&sample()).replace("\"control\":", "\"controller\":");
assert!(DesignerManifest::from_json(&json).is_err());
}
#[test]
fn a_truncated_document_is_refused() {
let json = manifest_to_json(&sample());
assert!(DesignerManifest::from_json(&json[..json.len() / 2]).is_err());
}
#[test]
fn every_token_maps_both_ways() {
for kind in [
PropertyValueKind::Bool,
PropertyValueKind::Int,
PropertyValueKind::UInt,
PropertyValueKind::Float,
PropertyValueKind::Number,
PropertyValueKind::String,
PropertyValueKind::Enum,
PropertyValueKind::Color,
PropertyValueKind::Rect,
] {
assert_eq!(value_kind_from_token(value_kind_token(kind)), Some(kind));
}
for shape in [
EventPayloadShape::Scalar,
EventPayloadShape::OptionalScalar,
EventPayloadShape::ListScalar,
EventPayloadShape::Tuple2,
EventPayloadShape::Tuple3,
EventPayloadShape::Tuple4,
EventPayloadShape::OptionalTuple2,
EventPayloadShape::Mixed,
] {
assert_eq!(shape_from_token(shape_token(shape)), Some(shape));
}
assert_eq!(value_kind_from_token("no_such_kind"), None);
assert_eq!(shape_from_token("no_such_shape"), None);
}
}