use crate::error::Error;
use crate::figma_schema;
use crate::http_client;
use crate::proxy_config::ProxyConfig;
use log::{info, warn};
use std::collections::{HashMap, HashSet};
const BASE_FILE_URL: &str = "https://api.figma.com/v1/files/";
#[derive(Clone, Debug)]
pub struct LibraryResolver {
api_key: String,
proxy_config: ProxyConfig,
dependency_graph: HashMap<String, HashSet<String>>,
library_versions: HashMap<String, String>,
}
impl LibraryResolver {
pub fn new(api_key: String, proxy_config: ProxyConfig) -> Self {
LibraryResolver {
api_key,
proxy_config,
dependency_graph: HashMap::new(),
library_versions: HashMap::new(),
}
}
pub fn discover_component_dependencies(
&mut self,
primary_doc_id: &str,
components: &HashMap<String, figma_schema::Component>,
) -> HashSet<String> {
let mut library_doc_ids: HashSet<String> = HashSet::new();
for (_id, component) in components {
if component.key.is_empty() {
continue;
}
if component.key.contains('/') {
if let Some(doc_id) = component.key.split('/').next() {
if doc_id != primary_doc_id {
library_doc_ids.insert(doc_id.to_string());
}
}
}
}
if !library_doc_ids.is_empty() {
info!(
"Discovered {} library dependencies for doc {}",
library_doc_ids.len(),
primary_doc_id
);
self.dependency_graph
.entry(primary_doc_id.to_string())
.or_default()
.extend(library_doc_ids.iter().cloned());
}
library_doc_ids
}
pub fn discover_variable_dependencies(
&mut self,
primary_doc_id: &str,
variables_response: &figma_schema::VariablesResponse,
) -> HashSet<String> {
let mut library_doc_ids: HashSet<String> = HashSet::new();
for (_id, collection) in &variables_response.meta.variable_collections {
if collection.remote {
if collection.key.contains('/') {
if let Some(doc_id) = collection.key.split('/').next() {
if doc_id != primary_doc_id {
library_doc_ids.insert(doc_id.to_string());
}
}
}
}
}
for (_id, variable) in &variables_response.meta.variables {
let common = match variable {
figma_schema::Variable::Boolean { common, .. }
| figma_schema::Variable::Float { common, .. }
| figma_schema::Variable::String { common, .. }
| figma_schema::Variable::Color { common, .. } => common,
};
if common.remote {
if common.key.contains('/') {
if let Some(doc_id) = common.key.split('/').next() {
if doc_id != primary_doc_id {
library_doc_ids.insert(doc_id.to_string());
}
}
}
}
}
if !library_doc_ids.is_empty() {
self.dependency_graph
.entry(primary_doc_id.to_string())
.or_default()
.extend(library_doc_ids.iter().cloned());
}
library_doc_ids
}
pub fn fetch_library_variables(
&self,
library_doc_id: &str,
) -> Result<figma_schema::VariablesResponse, Error> {
let url = format!("{}{}/variables/local", BASE_FILE_URL, library_doc_id);
let body = http_client::http_fetch(&self.api_key, url, &self.proxy_config)?;
let response: figma_schema::VariablesResponse = serde_json::from_str(&body)?;
Ok(response)
}
pub fn fetch_all_library_variables(
&self,
primary_doc_id: &str,
) -> HashMap<String, figma_schema::VariablesResponse> {
let mut results = HashMap::new();
let library_ids = match self.dependency_graph.get(primary_doc_id) {
Some(ids) => ids.clone(),
None => return results,
};
if library_ids.is_empty() {
return results;
}
info!("Fetching variables from {} library dependencies", library_ids.len());
let requests: Vec<http_client::BatchRequest> = library_ids
.iter()
.map(|doc_id| http_client::BatchRequest {
id: doc_id.clone(),
url: format!("{}{}/variables/local", BASE_FILE_URL, doc_id),
})
.collect();
let responses = http_client::http_fetch_batch(&self.api_key, requests, &self.proxy_config);
for response in responses {
match response.result {
Ok(body) => match serde_json::from_str::<figma_schema::VariablesResponse>(&body) {
Ok(vars) => {
results.insert(response.id, vars);
}
Err(e) => {
warn!("Failed to parse variables for library doc {}: {:?}", response.id, e);
}
},
Err(e) => {
warn!("Failed to fetch variables for library doc {}: {:?}", response.id, e);
}
}
}
info!(
"Successfully fetched variables from {}/{} library dependencies",
results.len(),
library_ids.len()
);
results
}
pub fn dependencies(&self) -> &HashMap<String, HashSet<String>> {
&self.dependency_graph
}
pub fn library_ids(&self, primary_doc_id: &str) -> HashSet<String> {
self.dependency_graph.get(primary_doc_id).cloned().unwrap_or_default()
}
pub fn add_dependency(&mut self, primary_doc_id: &str, library_doc_id: &str) {
self.dependency_graph
.entry(primary_doc_id.to_string())
.or_default()
.insert(library_doc_id.to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_dependency() {
let mut resolver = LibraryResolver::new("key".to_string(), ProxyConfig::None);
resolver.add_dependency("doc1", "lib1");
resolver.add_dependency("doc1", "lib2");
let libs = resolver.library_ids("doc1");
assert_eq!(libs.len(), 2);
assert!(libs.contains("lib1"));
assert!(libs.contains("lib2"));
}
#[test]
fn test_no_dependencies() {
let resolver = LibraryResolver::new("key".to_string(), ProxyConfig::None);
assert!(resolver.library_ids("doc1").is_empty());
}
}