Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/app/src/host/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ pub struct PresetsImported {
/// Import source recognized by the preset parser.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportFormat {
/// Native v1 Chipmunk named preset document.
/// Version 1 Chipmunk named preset document.
Version1,
/// Native v2 Chipmunk named preset document.
/// Version 2 Chipmunk named preset document.
Version2,
/// Legacy Chipmunk V3 TypeScript frontend export.
Legacy,
Expand Down
10 changes: 5 additions & 5 deletions crates/app/src/host/service/presets_io/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ fn parse_legacy_collection(content: &str) -> Result<LegacyCollectionOutcome, Jso
Ok(outcome)
}

/// Converts one stringified legacy filter entry into a native filter row snapshot.
/// Converts one stringified legacy filter entry into a preset filter row snapshot.
fn parse_legacy_filter(payload: &str, index: usize) -> Result<PresetFilterEntry, JsonError> {
let value: Value = serde_json::from_str(payload)?;
let filter = value
Expand Down Expand Up @@ -292,15 +292,15 @@ fn parse_legacy_filter(payload: &str, index: usize) -> Result<PresetFilterEntry,
Ok(entry)
}

/// Converts one stringified legacy chart entry into a native search-value row snapshot.
/// Converts one stringified legacy chart entry into a preset search-value row snapshot.
fn parse_legacy_chart(payload: &str, index: usize) -> Result<PresetSearchValueEntry, JsonError> {
let value: Value = serde_json::from_str(payload)?;
let text = value
.get("filter")
.and_then(Value::as_str)
.ok_or_else(|| JsonError::io(IoError::other("missing chart filter")))?;
// Legacy chart entries are really regex-backed search values, not literal
// filters, so they map to the search-value side of the native model.
// filters, so they map to the search-value side of the preset model.
let filter = SearchFilter::plain(text).regex(true).ignore_case(true);
if !validate_search_value_filter(&filter).is_eligible() {
return Err(JsonError::io(IoError::other("invalid legacy chart")));
Expand Down Expand Up @@ -369,7 +369,7 @@ fn parse_legacy_filter_colors(value: &Value, index: usize) -> ColorPair {
ColorPair::new(fg, bg)
}

/// Reads a legacy chart color, defaulting to the native search-value palette.
/// Reads a legacy chart color, defaulting to the search-value palette.
fn parse_legacy_chart_color(value: &Value, index: usize) -> Color32 {
let default = colors::search_value_color(index);
let Some(color) = value.get("color") else {
Expand All @@ -387,7 +387,7 @@ fn parse_legacy_chart_color(value: &Value, index: usize) -> Color32 {
})
}

/// Returns the native default filter color pair for a legacy row index.
/// Returns the default filter color pair for a legacy row index.
fn default_filter_colors(index: usize) -> ColorPair {
colors::FILTER_HIGHLIGHT_COLORS[index % colors::FILTER_HIGHLIGHT_COLORS.len()].clone()
}
Expand Down
28 changes: 14 additions & 14 deletions crates/app/src/host/service/presets_io/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Service-side import and export for named preset documents.
//!
//! This module owns the native on-disk JSON schema, parses the legacy export
//! This module owns the versioned on-disk JSON schema, parses the legacy export
//! shape for backward compatibility, and validates imported filters before the
//! UI applies them into the runtime preset registry.

Expand Down Expand Up @@ -72,21 +72,21 @@ pub enum LegacyEntryKind {
Unsupported(String),
}

/// Validate then serializes a preset snapshot into the native named-presets
/// Validate then serializes a preset snapshot into the versioned named-presets
/// JSON document.
pub fn serialize_named_presets(presets: Vec<Preset>) -> Result<String, String> {
v2::serialize_native_v2_presets(presets)
v2::serialize_presets(presets)
}

/// Parses a native preset document or a supported legacy export.
/// Parses a versioned preset document or a supported legacy export.
///
/// Returned presets already have fresh runtime ids assigned so the UI can hand
/// them to the registry import path directly.
pub fn import_named_presets(text: &str) -> Result<ImportReport, String> {
let value = parse_root_value(text)?;
let (format, presets, warnings) = match value {
Value::Object(root) => {
let (format, presets) = parse_native_document_from_value(root)?;
let (format, presets) = parse_versioned_document(root)?;
(format, presets, Vec::new())
}
Value::Array(items) => {
Expand All @@ -108,18 +108,18 @@ pub fn import_named_presets(text: &str) -> Result<ImportReport, String> {
fn parse_root_value(text: &str) -> Result<Value, String> {
let trimmed = text.trim_start();
serde_json::from_str(text).map_err(|err| {
// The legacy export uses a top-level array while the native format uses
// The legacy export uses a top-level array while the versioned format uses
// an object, so the first non-whitespace token is enough to classify
// syntax failures for the user-facing error.
if trimmed.starts_with('[') {
format!("invalid legacy preset export: {err}")
} else {
format!("invalid native preset document: {err}")
format!("invalid preset document: {err}")
}
})
}

fn parse_native_document_from_value(
fn parse_versioned_document(
root: serde_json::Map<String, Value>,
) -> Result<(ImportFormat, Vec<Preset>), String> {
let kind = root
Expand All @@ -138,18 +138,18 @@ fn parse_native_document_from_value(

match version {
1 => {
let presets = v1::parse_native_v1_document(root)?;
let presets = v1::parse_document(root)?;
Ok((ImportFormat::Version1, presets))
}
DOCUMENT_VERSION => {
let presets = v2::parse_native_v2_document(root)?;
let presets = v2::parse_document(root)?;
Ok((ImportFormat::Version2, presets))
}
_ => Err(format!("unsupported preset document version: {version}")),
}
}

/// Validates that a preset name is usable in native imports and exports.
/// Validates that a preset name is usable in versioned imports and exports.
pub fn validate_name(name: &str) -> Result<(), String> {
if name.trim().is_empty() {
return Err("preset name cannot be blank".to_owned());
Expand Down Expand Up @@ -218,18 +218,18 @@ mod tests {
use super::*;

#[test]
fn native_rejects_kind() {
fn rejects_unsupported_document_kind() {
import_named_presets(r#"{"kind":"wrong","version":2,"presets":[]}"#).unwrap_err();
}

#[test]
fn native_rejects_version() {
fn rejects_unsupported_document_version() {
import_named_presets(r#"{"kind":"chipmunk_named_presets","version":3,"presets":[]}"#)
.unwrap_err();
}

#[test]
fn import_rejects_object_without_native_kind() {
fn import_rejects_object_without_document_kind() {
import_named_presets(r#"{"presets":[]}"#).unwrap_err();
}
}
33 changes: 14 additions & 19 deletions crates/app/src/host/service/presets_io/v1.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Native v1 preset import compatibility.
//! Version 1 preset document import compatibility.
//!
//! Unlike v2, v1 stores only filter/search-value definitions. Row enabled
//! state and colors are not present, so conversion immediately applies runtime
Expand Down Expand Up @@ -29,38 +29,33 @@ use crate::host::ui::registry::presets::Preset;

use super::{validate_filter_entry, validate_name, validate_search_value_entry};

/// Preset payload stored in native v1 preset documents.
/// Preset payload stored in v1 preset documents.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct NativePresetV1 {
struct DocumentPreset {
name: String,
filters: Vec<SearchFilter>,
search_values: Vec<SearchFilter>,
}

#[derive(Debug, Serialize, Deserialize)]
struct NativeDocumentV1 {
struct PresetDocument {
kind: String,
version: u8,
presets: Vec<NativePresetV1>,
presets: Vec<DocumentPreset>,
}

/// Parses a native v1 document object into runtime presets with default row state.
pub fn parse_native_v1_document(
root: serde_json::Map<String, Value>,
) -> Result<Vec<Preset>, String> {
let document: NativeDocumentV1 = serde_json::from_value(Value::Object(root))
.map_err(|err| format!("invalid native preset document: {err}"))?;
document
.presets
.iter()
.try_for_each(validate_native_preset_v1)?;
/// Parses a v1 document object into runtime presets with default row state.
pub fn parse_document(root: serde_json::Map<String, Value>) -> Result<Vec<Preset>, String> {
let document: PresetDocument = serde_json::from_value(Value::Object(root))
.map_err(|err| format!("invalid preset document: {err}"))?;
document.presets.iter().try_for_each(validate_preset)?;
let presets = document.presets.into_iter().map(Preset::from).collect();

Ok(presets)
}

fn validate_native_preset_v1(preset: &NativePresetV1) -> Result<(), String> {
let NativePresetV1 {
fn validate_preset(preset: &DocumentPreset) -> Result<(), String> {
let DocumentPreset {
name,
filters,
search_values,
Expand All @@ -79,8 +74,8 @@ fn validate_native_preset_v1(preset: &NativePresetV1) -> Result<(), String> {
Ok(())
}

impl From<NativePresetV1> for Preset {
fn from(value: NativePresetV1) -> Self {
impl From<DocumentPreset> for Preset {
fn from(value: DocumentPreset) -> Self {
Preset::with_default_state(
Uuid::new_v4(),
value.name,
Expand Down
Loading
Loading