use crate::parse::{
FvarAxis, FvarData, FvarInstance, StatAxis, StatAxisValue, StatCombination, StatData,
};
use crate::selection::{FaceClassification, FamilyScenario, InstanceInfo, VfRecipe};
use crate::selection_italic::{ItalicCapabilityMap, ItalicKind};
#[derive(Debug, Clone, serde::Serialize)]
pub struct ItalicKindJson {
pub kind: String,
}
impl From<ItalicKind> for ItalicKindJson {
fn from(kind: ItalicKind) -> Self {
Self {
kind: match kind {
ItalicKind::Normal => "normal".to_string(),
ItalicKind::Italic => "italic".to_string(),
},
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct VfRecipeJson {
pub axis_values: Vec<AxisValue>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct AxisValue {
pub tag: String,
pub value: f32,
}
impl From<VfRecipe> for VfRecipeJson {
fn from(recipe: VfRecipe) -> Self {
Self {
axis_values: recipe
.axis_values
.into_iter()
.map(|(tag, value)| AxisValue { tag, value })
.collect(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct InstanceInfoJson {
pub italic_instances: Vec<String>,
pub ps_name: String,
pub style_name: String,
}
impl From<InstanceInfo> for InstanceInfoJson {
fn from(info: InstanceInfo) -> Self {
Self {
italic_instances: info.italic_instances,
ps_name: info.ps_name,
style_name: info.style_name,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FaceClassificationJson {
pub italic_kind: ItalicKindJson,
pub vf_recipe: Option<VfRecipeJson>,
pub weight_key: u16,
pub stretch_key: u16,
pub is_variable: bool,
pub instance_info: Option<InstanceInfoJson>,
}
impl From<FaceClassification> for FaceClassificationJson {
fn from(classification: FaceClassification) -> Self {
Self {
italic_kind: classification.font_style.into(),
vf_recipe: classification.vf_recipe.map(|r| r.into()),
weight_key: classification.weight_key,
stretch_key: classification.stretch_key,
is_variable: classification.is_variable,
instance_info: classification.instance_info.map(|i| i.into()),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FamilyScenarioJson {
pub scenario: String,
}
impl From<FamilyScenario> for FamilyScenarioJson {
fn from(scenario: FamilyScenario) -> Self {
Self {
scenario: match scenario {
FamilyScenario::SingleStatic => "single_static".to_string(),
FamilyScenario::MultiStatic => "multi_static".to_string(),
FamilyScenario::SingleVf => "single_vf".to_string(),
FamilyScenario::DualVf => "dual_vf".to_string(),
},
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FaceOrVfWithRecipeJson {
pub face_id: String,
pub vf_recipe: Option<VfRecipeJson>,
pub instance_info: Option<InstanceInfoJson>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ItalicCapabilityMapJson {
pub upright_slots: Vec<UprightSlot>,
pub italic_slots: Vec<ItalicSlot>,
pub scenario: FamilyScenarioJson,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct UprightSlot {
pub weight_key: u16,
pub stretch_key: u16,
pub face_id: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ItalicSlot {
pub weight_key: u16,
pub stretch_key: u16,
pub face: FaceOrVfWithRecipeJson,
}
impl From<ItalicCapabilityMap> for ItalicCapabilityMapJson {
fn from(map: ItalicCapabilityMap) -> Self {
Self {
upright_slots: map
.upright_slots
.into_iter()
.map(|((weight_key, stretch_key), face)| UprightSlot {
weight_key,
stretch_key,
face_id: face.face_id,
})
.collect(),
italic_slots: map
.italic_slots
.into_iter()
.map(|((weight_key, stretch_key), face)| ItalicSlot {
weight_key,
stretch_key,
face: FaceOrVfWithRecipeJson {
face_id: face.face_id,
vf_recipe: face.vf_recipe.map(|r| r.into()),
instance_info: face.instance_info.map(|i| i.into()),
},
})
.collect(),
scenario: map.scenario.into(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FvarAxisJson {
pub tag: String,
pub min: f32,
pub def: f32,
pub max: f32,
pub flags: u16,
pub name: String,
}
impl From<FvarAxis> for FvarAxisJson {
fn from(axis: FvarAxis) -> Self {
Self {
tag: axis.tag,
min: axis.min,
def: axis.def,
max: axis.max,
flags: axis.flags,
name: axis.name,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FvarInstanceJson {
pub name: String,
pub coordinates: Vec<AxisValue>,
pub flags: u16,
pub postscript_name: Option<String>,
}
impl From<FvarInstance> for FvarInstanceJson {
fn from(instance: FvarInstance) -> Self {
Self {
name: instance.name,
coordinates: instance
.coordinates
.into_iter()
.map(|(tag, value)| AxisValue { tag, value })
.collect(),
flags: instance.flags,
postscript_name: instance.postscript_name,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FvarDataJson {
pub axes: Vec<FvarAxisJson>,
pub instances: Vec<FvarInstanceJson>,
}
impl From<FvarData> for FvarDataJson {
fn from(data: FvarData) -> Self {
Self {
axes: data.axes.into_iter().map(|(_, axis)| axis.into()).collect(),
instances: data
.instances
.into_iter()
.map(|instance| instance.into())
.collect(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StatAxisValueJson {
pub name: String,
pub value: f32,
pub linked_value: Option<f32>,
pub range_min_value: Option<f32>,
pub range_max_value: Option<f32>,
}
impl From<StatAxisValue> for StatAxisValueJson {
fn from(value: StatAxisValue) -> Self {
Self {
name: value.name,
value: value.value,
linked_value: value.linked_value,
range_min_value: value.range_min_value,
range_max_value: value.range_max_value,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StatAxisJson {
pub tag: String,
pub name: String,
pub values: Vec<StatAxisValueJson>,
}
impl From<StatAxis> for StatAxisJson {
fn from(axis: StatAxis) -> Self {
Self {
tag: axis.tag,
name: axis.name,
values: axis.values.into_iter().map(|value| value.into()).collect(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StatCombinationJson {
pub name: String,
pub values: Vec<AxisValue>,
}
impl From<StatCombination> for StatCombinationJson {
fn from(combination: StatCombination) -> Self {
Self {
name: combination.name,
values: combination
.values
.into_iter()
.map(|(tag, value)| AxisValue { tag, value })
.collect(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StatDataJson {
pub axes: Vec<StatAxisJson>,
pub combinations: Vec<StatCombinationJson>,
pub elided_fallback_name: Option<String>,
}
impl From<StatData> for StatDataJson {
fn from(data: StatData) -> Self {
Self {
axes: data.axes.into_iter().map(|axis| axis.into()).collect(),
combinations: data
.combinations
.into_iter()
.map(|combination| combination.into())
.collect(),
elided_fallback_name: data.elided_fallback_name,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FontAnalysisResponse {
pub classifications: Vec<FaceClassificationJson>,
pub capability_map: ItalicCapabilityMapJson,
pub fvar_data: Option<FvarDataJson>,
pub stat_data: Option<StatDataJson>,
pub metadata: AnalysisMetadata,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct AnalysisMetadata {
pub face_count: usize,
pub has_variable_fonts: bool,
pub timestamp: String,
pub engine_version: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SuccessResponse<T> {
pub success: bool,
pub data: T,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ErrorResponse {
pub code: String,
pub message: String,
pub details: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ErrorResponseWrapper {
pub success: bool,
pub error: ErrorResponse,
}
pub mod utils {
use super::*;
pub fn success_response<T: serde::Serialize>(data: T) -> SuccessResponse<T> {
SuccessResponse {
success: true,
data,
}
}
pub fn error_response(code: &str, message: &str) -> ErrorResponseWrapper {
ErrorResponseWrapper {
success: false,
error: ErrorResponse {
code: code.to_string(),
message: message.to_string(),
details: None,
},
}
}
pub fn error_response_with_details(
code: &str,
message: &str,
details: &str,
) -> ErrorResponseWrapper {
ErrorResponseWrapper {
success: false,
error: ErrorResponse {
code: code.to_string(),
message: message.to_string(),
details: Some(details.to_string()),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::selection_italic::ItalicKind;
use crate::{
FaceClassification, FamilyScenario, FvarAxis, FvarData, FvarInstance, InstanceInfo,
StatAxis, StatAxisValue, StatCombination, StatData, VfRecipe,
};
use std::collections::HashMap;
#[test]
fn test_italic_kind_json_serialization() {
let normal = ItalicKind::Normal;
let normal_json = ItalicKindJson::from(normal);
let json_str = serde_json::to_string(&normal_json).unwrap();
assert_eq!(json_str, r#"{"kind":"normal"}"#);
let italic = ItalicKind::Italic;
let italic_json = ItalicKindJson::from(italic);
let json_str = serde_json::to_string(&italic_json).unwrap();
assert_eq!(json_str, r#"{"kind":"italic"}"#);
}
#[test]
fn test_vf_recipe_json_serialization() {
let mut axis_values = HashMap::new();
axis_values.insert("ital".to_string(), 1.0);
axis_values.insert("wght".to_string(), 400.0);
let recipe = VfRecipe { axis_values };
let recipe_json = VfRecipeJson::from(recipe);
let json_str = serde_json::to_string(&recipe_json).unwrap();
assert!(json_str.contains("\"tag\":\"ital\""));
assert!(json_str.contains("\"value\":1.0"));
assert!(json_str.contains("\"tag\":\"wght\""));
assert!(json_str.contains("\"value\":400.0"));
}
#[test]
fn test_face_classification_json_serialization() {
let classification = FaceClassification {
font_style: ItalicKind::Italic,
vf_recipe: Some(VfRecipe::new("ital", 1.0)),
weight_key: 400,
stretch_key: 5,
is_variable: true,
instance_info: Some(InstanceInfo {
italic_instances: vec!["Italic".to_string()],
ps_name: "TestFont-Italic".to_string(),
style_name: "Italic".to_string(),
}),
};
let classification_json = FaceClassificationJson::from(classification);
let json_str = serde_json::to_string(&classification_json).unwrap();
assert!(json_str.contains("\"italic_kind\":{\"kind\":\"italic\"}"));
assert!(json_str.contains("\"weight_key\":400"));
assert!(json_str.contains("\"stretch_key\":5"));
assert!(json_str.contains("\"is_variable\":true"));
assert!(json_str.contains("\"TestFont-Italic\""));
}
#[test]
fn test_family_scenario_json_serialization() {
let scenarios = vec![
FamilyScenario::SingleStatic,
FamilyScenario::MultiStatic,
FamilyScenario::SingleVf,
FamilyScenario::DualVf,
];
for scenario in scenarios {
let scenario_json = FamilyScenarioJson::from(scenario.clone());
let json_str = serde_json::to_string(&scenario_json).unwrap();
let expected = match scenario {
FamilyScenario::SingleStatic => "single_static",
FamilyScenario::MultiStatic => "multi_static",
FamilyScenario::SingleVf => "single_vf",
FamilyScenario::DualVf => "dual_vf",
};
assert!(json_str.contains(&format!("\"scenario\":\"{}\"", expected)));
}
}
#[test]
fn test_utils_functions() {
let data = "test_data".to_string();
let success = utils::success_response(data);
let success_json = serde_json::to_string(&success).unwrap();
assert!(success_json.contains("\"success\":true"));
assert!(success_json.contains("\"data\":\"test_data\""));
let error = utils::error_response("TEST_ERROR", "Test error message");
let error_json = serde_json::to_string(&error).unwrap();
assert!(error_json.contains("\"success\":false"));
assert!(error_json.contains("\"code\":\"TEST_ERROR\""));
assert!(error_json.contains("\"message\":\"Test error message\""));
let error_with_details = utils::error_response_with_details(
"DETAILED_ERROR",
"Error message",
"Additional details",
);
let error_with_details_json = serde_json::to_string(&error_with_details).unwrap();
assert!(error_with_details_json.contains("\"success\":false"));
assert!(error_with_details_json.contains("\"code\":\"DETAILED_ERROR\""));
assert!(error_with_details_json.contains("\"message\":\"Error message\""));
assert!(error_with_details_json.contains("\"details\":\"Additional details\""));
}
#[test]
fn test_fvar_data_json_serialization() {
let mut axes = HashMap::new();
axes.insert(
"ital".to_string(),
FvarAxis {
tag: "ital".to_string(),
min: 0.0,
def: 0.0,
max: 1.0,
flags: 0,
name: "Italic".to_string(),
},
);
let instances = vec![FvarInstance {
name: "Regular".to_string(),
coordinates: {
let mut coords = HashMap::new();
coords.insert("ital".to_string(), 0.0);
coords.insert("wght".to_string(), 400.0);
coords
},
flags: 0,
postscript_name: Some("TestFont-Regular".to_string()),
}];
let fvar_data = FvarData { axes, instances };
let fvar_json = FvarDataJson::from(fvar_data);
let json_str = serde_json::to_string(&fvar_json).unwrap();
assert!(json_str.contains("\"tag\":\"ital\""));
assert!(json_str.contains("\"name\":\"Regular\""));
assert!(json_str.contains("\"TestFont-Regular\""));
}
#[test]
fn test_stat_data_json_serialization() {
let stat_data = StatData {
axes: vec![StatAxis {
tag: "ital".to_string(),
name: "Italic".to_string(),
values: vec![
StatAxisValue {
name: "Roman".to_string(),
value: 0.0,
linked_value: None,
range_min_value: None,
range_max_value: None,
},
StatAxisValue {
name: "Italic".to_string(),
value: 1.0,
linked_value: None,
range_min_value: None,
range_max_value: None,
},
],
}],
combinations: vec![StatCombination {
name: "Regular".to_string(),
values: vec![("ital".to_string(), 0.0)],
}],
elided_fallback_name: Some("TestFont".to_string()),
};
let stat_json = StatDataJson::from(stat_data);
let json_str = serde_json::to_string(&stat_json).unwrap();
assert!(json_str.contains("\"tag\":\"ital\""));
assert!(json_str.contains("\"name\":\"Regular\""));
assert!(json_str.contains("\"elided_fallback_name\":\"TestFont\""));
}
}