diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 72f1f1e42..4da5fbd5b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -248,7 +248,7 @@ jobs: - name: Build all run: cargo build --all-targets --all-features - name: Test all - run: cargo test --all-targets --all-features + run: cargo test --all-targets --features=reflection,fetch,dcf_info ############ Figma resources figma-resources: runs-on: ubuntu-latest diff --git a/.idea/runConfigurations/Fetch_HelloWorld.xml b/.idea/runConfigurations/Fetch_HelloWorld.xml new file mode 100644 index 000000000..5fcb9578a --- /dev/null +++ b/.idea/runConfigurations/Fetch_HelloWorld.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Figma_Fetch_Tests.xml b/.idea/runConfigurations/Figma_Fetch_Tests.xml new file mode 100644 index 000000000..d192fb2ec --- /dev/null +++ b/.idea/runConfigurations/Figma_Fetch_Tests.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index aa8c3b05b..40dcf4481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,7 +414,6 @@ dependencies = [ "layout", "lazy_static", "log", - "phf 0.11.2", "prost", "serde", "serde-generate", @@ -761,38 +760,18 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", + "phf_macros", + "phf_shared", "proc-macro-hack", ] -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros 0.11.2", - "phf_shared 0.11.2", -] - [[package]] name = "phf_generator" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ - "phf_shared 0.10.0", - "rand", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared 0.11.2", + "phf_shared", "rand", ] @@ -802,27 +781,14 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared", "proc-macro-hack", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "phf_shared" version = "0.10.0" @@ -832,15 +798,6 @@ dependencies = [ "siphasher 0.3.11", ] -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "png" version = "0.17.13" @@ -1137,7 +1094,7 @@ checksum = "f8c9331265d81c61212dc75df7b0836544ed8e32dba77a522f113805ff9a948e" dependencies = [ "heck 0.3.3", "include_dir", - "phf 0.10.1", + "phf", "serde", "serde-reflection", "textwrap", diff --git a/crates/dc_jni/src/jni.rs b/crates/dc_jni/src/jni.rs index 555129251..269b5305f 100644 --- a/crates/dc_jni/src/jni.rs +++ b/crates/dc_jni/src/jni.rs @@ -28,7 +28,7 @@ use jni::sys::{jint, JNI_VERSION_1_6}; use jni::{JNIEnv, JavaVM}; use lazy_static::lazy_static; -use log::{error, LevelFilter}; +use log::{error, info, LevelFilter}; lazy_static! { static ref JAVA_VM: Mutex>> = Mutex::new(None); @@ -126,9 +126,19 @@ fn jni_fetch_doc_impl( let request: ConvertRequest = serde_json::from_str(&request_json)?; let convert_result: figma_import::ConvertResponse = - match fetch_doc(&doc_id, &version_id, request, proxy_config).map_err(Error::from) { + match fetch_doc(&doc_id, &version_id, &request, proxy_config).map_err(Error::from) { Ok(it) => it, Err(err) => { + let queries_string = request + .queries + .iter() + .map(|q| format!("--nodes=\"{}\" ", q)) + .collect::>() + .join(" "); + + info!("Failed to fetch {}, Try fetching locally", doc_id); + info!("fetch --doc-id={} --version-id={} {} ", doc_id, version_id, queries_string); + map_err_to_exception(env, &err, doc_id).expect("Failed to throw exception"); return Err(err); } diff --git a/crates/figma_import/Cargo.toml b/crates/figma_import/Cargo.toml index 9852200cf..881d7825d 100644 --- a/crates/figma_import/Cargo.toml +++ b/crates/figma_import/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true [features] default = [] reflection = ["serde-reflection", "serde-generate", "clap"] -http_mock = ["phf"] fetch = ["clap"] dcf_info = ["clap"] fetch_layout = ["clap"] +test_fetches = ["fetch"] [dependencies] @@ -31,7 +31,6 @@ svgtypes.workspace = true unicode-segmentation.workspace = true image.workspace = true euclid.workspace = true -phf = { workspace = true, optional = true } # layout dependencies taffy.workspace = true diff --git a/crates/figma_import/src/document.rs b/crates/figma_import/src/document.rs index acb8dfca9..5711fc7b7 100644 --- a/crates/figma_import/src/document.rs +++ b/crates/figma_import/src/document.rs @@ -16,7 +16,6 @@ use dc_bundle::definition::element::{ variable_map::NameIdMap, Collection, Mode, Variable, VariableMap, }; use dc_bundle::legacy_definition::element::node::NodeQuery; -#[cfg(not(feature = "http_mock"))] use std::time::Duration; use std::{ collections::{HashMap, HashSet}, @@ -40,13 +39,11 @@ use dc_bundle::legacy_definition::EncodedImageMap; use dc_bundle::legacy_figma_live_update::FigmaDocInfo; use log::error; -#[cfg(not(feature = "http_mock"))] const FIGMA_TOKEN_HEADER: &str = "X-Figma-Token"; const BASE_FILE_URL: &str = "https://api.figma.com/v1/files/"; const BASE_COMPONENT_URL: &str = "https://api.figma.com/v1/components/"; const BASE_PROJECT_URL: &str = "https://api.figma.com/v1/projects/"; -#[cfg(not(feature = "http_mock"))] fn http_fetch(api_key: &str, url: String, proxy_config: &ProxyConfig) -> Result { let mut agent_builder = ureq::AgentBuilder::new(); let mut buffer = Vec::new(); @@ -69,9 +66,6 @@ fn http_fetch(api_key: &str, url: String, proxy_config: &ProxyConfig) -> Result< Ok(body) } -#[cfg(feature = "http_mock")] -use crate::figma_v1_document_mocks::http_fetch; - /// Document update requests return this value to indicate if an update was /// made or not. #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/crates/figma_import/src/fetch.rs b/crates/figma_import/src/fetch.rs index ff74a751e..50406b2ab 100644 --- a/crates/figma_import/src/fetch.rs +++ b/crates/figma_import/src/fetch.rs @@ -45,7 +45,7 @@ pub enum ProxyConfig { pub struct ConvertRequest<'r> { figma_api_key: &'r str, // Node names - queries: Vec<&'r str>, + pub queries: Vec<&'r str>, // Ignored images ignored_images: Vec>, @@ -65,6 +65,19 @@ pub struct ConvertRequest<'r> { image_session: Option, } +impl<'r> ConvertRequest<'r> { + pub fn new(figma_api_key: &'r str, queries: Vec<&'r str>, version: Option) -> Self { + Self { + figma_api_key, + queries, + ignored_images: Vec::new(), + last_modified: None, + version, + image_session: None, + } + } +} + #[derive(Serialize, Deserialize)] pub enum ConvertResponse { Document(Vec), @@ -74,7 +87,7 @@ pub enum ConvertResponse { pub fn fetch_doc( id: &str, requested_version_id: &str, - rq: ConvertRequest, + rq: &ConvertRequest, proxy_config: &ProxyConfig, ) -> Result { if let Some(mut doc) = Document::new_if_changed( @@ -82,8 +95,8 @@ pub fn fetch_doc( id.into(), requested_version_id.into(), proxy_config, - rq.last_modified.unwrap_or(String::new()), - rq.version.unwrap_or(String::new()), + rq.last_modified.clone().unwrap_or(String::new()), + rq.version.clone().unwrap_or(String::new()), rq.image_session.clone(), )? { // The document has changed since the version the client has, so we should fetch diff --git a/crates/figma_import/src/figma_schema.rs b/crates/figma_import/src/figma_schema.rs index 7064315e3..bd8d18cdd 100644 --- a/crates/figma_import/src/figma_schema.rs +++ b/crates/figma_import/src/figma_schema.rs @@ -1230,6 +1230,7 @@ pub struct VariableAlias { pub enum VariableAliasOrList { Alias(VariableAlias), List(Vec), + Map(HashMap), } impl VariableAliasOrList { fn get_name(&self) -> Option { @@ -1243,6 +1244,9 @@ impl VariableAliasOrList { return Some(alias.id.clone()); } } + VariableAliasOrList::Map(_) => { + panic!("We don't support Maps of Variables!"); + } } None } diff --git a/crates/figma_import/src/lib.rs b/crates/figma_import/src/lib.rs index 955565ec5..84da945a2 100644 --- a/crates/figma_import/src/lib.rs +++ b/crates/figma_import/src/lib.rs @@ -49,8 +49,6 @@ pub use dc_bundle::legacy_definition::element::node::NodeQuery; pub use dc_bundle::legacy_definition::view::view::View; pub use dc_bundle::legacy_definition::view::view::ViewData; -#[cfg(feature = "http_mock")] -mod figma_v1_document_mocks; /// Functionality related to reflection for deserializing our bincode archives in other /// languages #[cfg(feature = "reflection")] diff --git a/crates/figma_import/src/tools/fetch.rs b/crates/figma_import/src/tools/fetch.rs index c44ed524c..d725b691f 100644 --- a/crates/figma_import/src/tools/fetch.rs +++ b/crates/figma_import/src/tools/fetch.rs @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::io::Write; +use std::env; +use std::io::{Error, ErrorKind, Write}; use crate::{Document, ProxyConfig}; /// Utility program to fetch a doc and serialize it to file use clap::Parser; use dc_bundle::legacy_definition::element::node::NodeQuery; use dc_bundle::legacy_definition::{DesignComposeDefinition, DesignComposeDefinitionHeader}; +use log::error; #[derive(Debug)] #[allow(dead_code)] @@ -69,47 +71,58 @@ pub struct Args { pub output: std::path::PathBuf, } -pub fn fetch(args: Args) -> Result<(), ConvertError> { - let proxy_config: ProxyConfig = match args.http_proxy { - Some(x) => ProxyConfig::HttpProxyConfig(x), - None => ProxyConfig::None, - }; - - // If the API Key wasn't provided on the path or via env var, load it from ~/.config/figma_access_token - let api_key = args.api_key.unwrap_or_else(|| { - let config_path = std::path::Path::new(&std::env::var("HOME").unwrap()) - .join(".config") - .join("figma_access_token"); - - std::fs::read_to_string(config_path) - .expect("Could not read API key from ~/.config/figma_access_token") - .trim() - .parse() - .unwrap() - }); +//Loads a Figma access token from either the FIGMA_ACCESS_TOKEN environment variable or a file located at ~/.config/figma_access_token. +// Returns a Result where: +// Ok(token) contains the loaded token as a String, with leading and trailing whitespace removed. +// Err(Error) indicates an error occurred during the loading process. Possible errors include: +// Failure to read the HOME environment variable. +// Failure to read the token file (e.g., file not found, permission denied). +// This function prioritizes reading the token from the environment variable. If it's not set, it attempts to read the token from the specified file. The file path is constructed using the user's home directory. +pub fn load_figma_token() -> Result { + match env::var("FIGMA_ACCESS_TOKEN") { + Ok(token) => Ok(token), + Err(_) => { + let home_dir = match env::var("HOME") { + Ok(val) => val, + Err(_) => return Err(Error::new(ErrorKind::Other, "Could not read HOME from env")), + }; + let config_path = + std::path::Path::new(&home_dir).join(".config").join("figma_access_token"); + let token = match std::fs::read_to_string(config_path) { + Ok(token) => token.trim().to_string(), + Err(e) => { + return Err(Error::new( + ErrorKind::NotFound, + format!( + "Could not read Figma token from ~/.config/figma_access_token: {}", + e + ), + )) + } + }; + Ok(token) + } + } +} - let mut doc: Document = Document::new( - api_key.as_str(), - args.doc_id, - args.version_id.unwrap_or(String::new()), - &proxy_config, - None, - )?; +pub fn build_definition( + doc: &mut Document, + nodes: &Vec, +) -> Result { let mut error_list = Vec::new(); // Convert the requested nodes from the Figma doc. let views = doc.nodes( - &args.nodes.iter().map(|name| NodeQuery::name(name)).collect(), + &nodes.iter().map(|name| NodeQuery::name(name)).collect(), &Vec::new(), &mut error_list, )?; for error in error_list { eprintln!("Warning: {error}"); } - let variable_map = doc.build_variable_map(); // Build the serializable doc structure - let serializable_doc = DesignComposeDefinition { + Ok(DesignComposeDefinition { views, component_sets: doc.component_sets().clone(), images: doc.encoded_image_map(), @@ -117,8 +130,29 @@ pub fn fetch(args: Args) -> Result<(), ConvertError> { name: doc.get_name(), version: doc.get_version(), id: doc.get_document_id(), - variable_map: variable_map, + variable_map, + }) +} + +pub fn fetch(args: Args) -> Result<(), ConvertError> { + let proxy_config: ProxyConfig = match args.http_proxy { + Some(x) => ProxyConfig::HttpProxyConfig(x), + None => ProxyConfig::None, }; + + // If the API Key wasn't provided on the path or via env var, load it from env or ~/.config/figma_access_token + let api_key = args.api_key.unwrap_or_else(|| load_figma_token().unwrap()); + + let mut doc: Document = Document::new( + api_key.as_str(), + args.doc_id, + args.version_id.unwrap_or(String::new()), + &proxy_config, + None, + )?; + + let dc_definition = build_definition(&mut doc, &args.nodes)?; + println!("Fetched document"); println!(" DC Version: {}", DesignComposeDefinitionHeader::current().version); println!(" Doc ID: {}", doc.get_document_id()); @@ -128,7 +162,7 @@ pub fn fetch(args: Args) -> Result<(), ConvertError> { // We don't bother with serialization of image sessions with this tool. let mut output = std::fs::File::create(args.output)?; let header = bincode::serialize(&DesignComposeDefinitionHeader::current())?; - let doc = bincode::serialize(&serializable_doc)?; + let doc = bincode::serialize(&dc_definition)?; output.write_all(header.as_slice())?; output.write_all(doc.as_slice())?; Ok(()) diff --git a/crates/figma_import/tests/test_fetches.rs b/crates/figma_import/tests/test_fetches.rs new file mode 100644 index 000000000..90f7bcc62 --- /dev/null +++ b/crates/figma_import/tests/test_fetches.rs @@ -0,0 +1,43 @@ +/* + * 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. + */ +#![cfg(feature = "fetch")] + +use figma_import::tools::fetch::{build_definition, load_figma_token}; +use figma_import::{Document, ProxyConfig}; + +// Simply fetches and serializes a doc +#[test] +#[cfg_attr(not(feature = "test_fetches"), ignore)] +fn fetch_variable_modes() { + const DOC_ID: &str = "HhGxvL4aHhP8ALsLNz56TP"; + const QUERIES: &[&str] = &["#stage", "#Box"]; + let queries: Vec = QUERIES.iter().map(|s| s.to_string()).collect(); + + let figma_token = load_figma_token().unwrap(); + + let mut doc: Document = Document::new( + figma_token.as_str(), + DOC_ID.to_string(), + String::new(), + &ProxyConfig::None, + None, + ) + .unwrap(); + + let dc_definition = build_definition(&mut doc, &queries).unwrap(); + + bincode::serialize(&dc_definition).unwrap(); +}