blob: d83fe36f52424393b9ec55003decca1d625619d3 [file] [log] [blame]
use crate::variable::VariableMap;
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::design_compose_definition::{DesignComposeDefinition, DesignComposeDefinitionHeader};
use crate::view::View;
use crate::Error;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
use std::sync::Arc;
pub mod element;
pub mod layout;
pub mod modifier;
pub mod view;
pub static CURRENT_VERSION: u32 = 28;
impl DesignComposeDefinitionHeader {
pub fn current(
last_modified: String,
name: String,
response_version: String,
id: String,
) -> DesignComposeDefinitionHeader {
DesignComposeDefinitionHeader {
dc_version: CURRENT_VERSION,
last_modified,
name,
response_version,
id,
..Default::default()
}
}
pub fn current_version() -> u32 {
CURRENT_VERSION
}
}
impl fmt::Display for DesignComposeDefinitionHeader {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// NOTE: Using `write!` here instead of typical `format!`
// to keep newlines.
write!(
f,
"DC Version: {}\nDoc ID: {}\nName: {}\nLast Modified: {}\nResponse Version: {}",
&self.dc_version, &self.id, &self.name, &self.last_modified, &self.response_version
)
}
}
impl DesignComposeDefinition {
pub fn new_with_details(
views: HashMap<NodeQuery, View>,
images: EncodedImageMap,
component_sets: HashMap<String, String>,
variable_map: VariableMap,
) -> DesignComposeDefinition {
DesignComposeDefinition {
views: views.iter().map(|(k, v)| (k.encode(), v.to_owned())).collect(),
images: images.into(),
component_sets,
variable_map: Some(variable_map).into(),
..Default::default()
}
}
pub fn views(&self) -> Result<HashMap<NodeQuery, View>, Error> {
self.views
.iter()
.map(|(k, v)| NodeQuery::decode(k).map(|query| (query, v.clone())))
.collect::<Result<HashMap<NodeQuery, View>, Error>>()
}
}
impl fmt::Display for DesignComposeDefinition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// NOTE: Using `write!` here instead of typical `format!`
// to keep newlines.
write!(
f,
"Views: {}\nComponent Sets: {}",
self.views.keys().count(),
self.component_sets.keys().count()
)
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)]
pub enum NodeQuery {
/// Find the node by ID
NodeId(String),
/// Find the node by name
NodeName(String),
/// Node by name that is a variant, so the name may have multiple properties
/// The first string is the node name and the second is its parent's name
NodeVariant(String, String),
NodeComponentSet(String),
}
impl NodeQuery {
/// Construct a NodeQuery::NodeId from the given ID string.
pub fn id(id: impl ToString) -> NodeQuery {
NodeQuery::NodeId(id.to_string())
}
/// Construct a NodeQuery::NodeName from the given node name
pub fn name(name: impl ToString) -> NodeQuery {
NodeQuery::NodeName(name.to_string())
}
/// Construct a NodeQuery::NodeVariant from the given variant name and parent name
pub fn variant(name: impl ToString, parent: impl ToString) -> NodeQuery {
NodeQuery::NodeVariant(name.to_string(), parent.to_string())
}
/// Construct a NodeQuery::NodeComponentSet from the given node component set name
pub fn component_set(name: impl ToString) -> NodeQuery {
NodeQuery::NodeComponentSet(name.to_string())
}
pub fn encode(&self) -> String {
match self {
NodeQuery::NodeId(id) => format!("id:{}", id),
NodeQuery::NodeName(name) => format!("name:{}", name),
NodeQuery::NodeVariant(name, parent) => {
assert!(!name.contains('\x1f'));
format!("variant:{}\x1f{}", name, parent)
}
NodeQuery::NodeComponentSet(name) => format!("component_set:{}", name),
}
}
pub fn decode(s: &str) -> Result<NodeQuery, Error> {
let (query_type, query_value) =
s.split_once(':').ok_or_else(|| Error::InvalidNodeQuery { query: s.to_string() })?;
match query_type {
"id" => Ok(NodeQuery::NodeId(query_value.to_string())),
"name" => Ok(NodeQuery::NodeName(query_value.to_string())),
"variant" => {
let variant_parts: Vec<&str> = query_value.split('\x1f').collect();
if variant_parts.len() != 2 {
return Err(Error::InvalidNodeQuery { query: s.to_string() });
}
Ok(NodeQuery::NodeVariant(
variant_parts[0].to_string(),
variant_parts[1].to_string(),
))
}
"component_set" => Ok(NodeQuery::NodeComponentSet(query_value.to_string())),
_ => Err(Error::InvalidNodeQuery { query: s.to_string() }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_query_id() {
let query = NodeQuery::id("test_id");
assert_eq!(query, NodeQuery::NodeId("test_id".to_string()));
assert_eq!(query.encode(), "id:test_id");
assert_eq!(NodeQuery::decode("id:test_id").unwrap(), query);
}
#[test]
fn test_node_query_name() {
let query = NodeQuery::name("test_name");
assert_eq!(query, NodeQuery::NodeName("test_name".to_string()));
assert_eq!(query.encode(), "name:test_name");
assert_eq!(NodeQuery::decode("name:test_name").unwrap(), query);
}
#[test]
fn test_node_query_variant() {
let query = NodeQuery::variant("variant_name", "parent_name");
assert_eq!(
query,
NodeQuery::NodeVariant("variant_name".to_string(), "parent_name".to_string())
);
assert_eq!(query.encode(), "variant:variant_name\x1fparent_name");
assert_eq!(NodeQuery::decode("variant:variant_name\x1fparent_name").unwrap(), query);
}
#[test]
fn test_node_query_name_uses_name() {
let query = NodeQuery::name("name:deadbeef");
assert_eq!(query, NodeQuery::NodeName("name:deadbeef".to_string()));
assert_eq!(query.encode(), "name:name:deadbeef");
assert_eq!(NodeQuery::decode("name:name:deadbeef").unwrap(), query);
}
#[test]
fn test_node_query_component_set() {
let query = NodeQuery::component_set("component_set_name");
assert_eq!(query, NodeQuery::NodeComponentSet("component_set_name".to_string()));
assert_eq!(query.encode(), "component_set:component_set_name");
assert_eq!(NodeQuery::decode("component_set:component_set_name").unwrap(), query);
}
#[test]
fn test_node_query_from_string_invalid() {
assert!(NodeQuery::decode("invalid_query").is_err());
assert!(NodeQuery::decode("id").is_err());
assert!(NodeQuery::decode("variant:name").is_err()); // Missing parent
assert!(NodeQuery::decode("variant:name\x1fparent\x1fextra").is_err()); // Extra part
assert!(NodeQuery::decode("unknown:value").is_err()); // Unknown type
}
#[test]
#[should_panic]
fn test_node_query_variant_with_unit_separator() {
let name_with_separator = "name\x1fwith\x1fseparator";
let parent = "parent";
let query = NodeQuery::variant(name_with_separator, parent);
query.encode();
}
}
/// EncodedImageMap contains a mapping from ImageKey to network bytes. It can create an
/// ImageMap and is intended to be used when we want to use Figma-defined components but do
/// not want to communicate with the Figma service.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EncodedImageMap(pub HashMap<String, Arc<serde_bytes::ByteBuf>>);
impl EncodedImageMap {
pub fn map(&self) -> HashMap<String, Arc<serde_bytes::ByteBuf>> {
self.0.clone()
}
}
impl Into<HashMap<String, Vec<u8>>> for EncodedImageMap {
fn into(self) -> HashMap<String, Vec<u8>> {
self.0.iter().map(|(k, v)| (k.clone(), v.to_vec())).collect()
}
}