selective parsing

This commit is contained in:
2026-01-02 14:53:29 +00:00
parent 96b1e172ee
commit 17ea08caac
12 changed files with 511 additions and 226 deletions

192
DESIGN.md
View File

@@ -1,192 +0,0 @@
# Cursebreaker Unity Parser - Design Document
## Project Overview
A high-performance Rust library for parsing and querying Unity project files (.unity scenes, .prefab prefabs, and .asset ScriptableObjects).
## Goals
1. **Parse Unity YAML Format**: Handle Unity's YAML 1.1 format with custom tags (`!u!`) and file ID references
2. **Extract Structure**: Parse GameObjects, Components, and their properties into queryable data structures
3. **High Performance**: Optimized for large Unity projects with minimal memory footprint
4. **Type Safety**: Strong typing for Unity's component system
5. **Library-First**: Designed as a reusable SDK for other Rust tools
## Target File Formats
- `.unity` - Unity scene files
- `.prefab` - Unity prefab files
- `.asset` - Unity ScriptableObject and other asset files
All three formats share the same underlying YAML structure with Unity-specific extensions.
## Unity File Format Structure
Unity files use YAML 1.1 with special conventions:
```yaml
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1866116814460599870
GameObject:
m_ObjectHideFlags: 0
m_Component:
- component: {fileID: 8151827567463220614}
- component: {fileID: 8755205353704683373}
m_Name: CardGrabber
--- !u!224 &8151827567463220614
RectTransform:
m_GameObject: {fileID: 1866116814460599870}
m_LocalPosition: {x: 0, y: 0, z: 0}
```
### Key Concepts
1. **Documents**: Each `---` starts a new YAML document representing a Unity object
2. **Type Tags**: `!u!N` indicates Unity type (e.g., `!u!1` = GameObject, `!u!224` = RectTransform)
3. **Anchors**: `&ID` defines a local file ID for the object
4. **File References**: `{fileID: N}` references objects by their ID (local or external)
5. **GUID References**: `{guid: ...}` references external assets
6. **Properties**: All Unity objects have serialized fields (usually prefixed with `m_`)
## Architecture
### Core Components
```
cursebreaker-parser/
├── src/
│ ├── lib.rs # Public API exports
│ ├── parser/ # YAML parsing layer
│ │ ├── mod.rs
│ │ ├── yaml.rs # YAML document parser
│ │ ├── unity_tag.rs # Unity type tag handler (!u!)
│ │ └── reference.rs # FileID/GUID reference parser
│ ├── model/ # Data model
│ │ ├── mod.rs
│ │ ├── document.rs # UnityDocument struct
│ │ ├── object.rs # UnityObject base
│ │ ├── gameobject.rs # GameObject type
│ │ ├── component.rs # Component types
│ │ └── property.rs # Property value types
│ ├── types/ # Unity type system
│ │ ├── mod.rs
│ │ ├── type_id.rs # Unity type ID -> name mapping
│ │ └── component_types.rs
│ ├── query/ # Query API
│ │ ├── mod.rs
│ │ ├── project.rs # UnityProject (multi-file)
│ │ ├── find.rs # Find objects/components
│ │ └── filter.rs # Filter/search utilities
│ └── error.rs # Error types
```
### Data Model
```rust
// Core types
pub struct UnityFile {
pub path: PathBuf,
pub documents: Vec<UnityDocument>,
}
pub struct UnityDocument {
pub type_id: u32, // From !u!N
pub file_id: i64, // From &ID
pub class_name: String, // E.g., "GameObject"
pub properties: PropertyMap,
}
pub struct UnityProject {
pub files: HashMap<PathBuf, UnityFile>,
// Reference resolution cache
}
// Property values (simplified)
pub enum PropertyValue {
Integer(i64),
Float(f64),
String(String),
Boolean(bool),
FileRef { file_id: i64, guid: Option<String> },
Vector3 { x: f64, y: f64, z: f64 },
Color { r: f64, g: f64, b: f64, a: f64 },
Array(Vec<PropertyValue>),
Object(PropertyMap),
}
```
## Performance Considerations
1. **Streaming Parser**: Parse YAML incrementally rather than loading entire file into memory
2. **Lazy Loading**: Only parse files when accessed
3. **Reference Caching**: Cache resolved references to avoid repeated lookups
4. **Zero-Copy Where Possible**: Use string slices and borrowed data where feasible
5. **Parallel Parsing**: Support parsing multiple files concurrently
## Dependencies
- `yaml-rust2` or `serde_yaml` - YAML parsing (evaluate both)
- `serde` - Serialization/deserialization
- `rayon` - Parallel processing (optional, for multi-file parsing)
- `thiserror` - Error handling
- `indexmap` - Ordered maps for properties
## Testing Strategy
1. **Unit Tests**: Each parser component tested independently
2. **Integration Tests**: Full file parsing with real Unity files
3. **Sample Data**: Use PiratePanic project as test corpus
4. **Benchmarks**: Performance tests on large Unity projects
5. **Fuzzing**: Fuzz testing for parser robustness (future)
## API Design Goals
### Simple File Parsing
```rust
let file = UnityFile::from_path("Scene.unity")?;
for doc in &file.documents {
println!("{}: {}", doc.class_name, doc.file_id);
}
```
### Query API
```rust
let project = UnityProject::from_directory("Assets/")?;
// Find all GameObjects
let objects = project.find_all_by_type("GameObject");
// Find by name
let player = project.find_by_name("Player")?;
// Get components
let transform = player.get_component("Transform")?;
let position = transform.get_vector3("m_LocalPosition")?;
```
### Reference Resolution
```rust
// Follow references automatically
let gameobject = project.get_object(file_id)?;
let transform_ref = gameobject.get_file_ref("m_Component[0].component")?;
let transform = project.resolve_reference(transform_ref)?;
```
## Future Enhancements (Out of Scope for v1)
- Unity YAML serialization (writing files)
- C# script parsing
- Asset dependency graphs
- Unity version detection and compatibility
- Binary .unity format support (older Unity versions)
- Meta file parsing (.meta files)
## Success Criteria
1. Successfully parse all files in PiratePanic sample project
2. Extract all GameObjects and Components with properties
3. Resolve all internal file references correctly
4. Parse large scene files (&gt;10MB) in &lt;100ms
5. Memory usage scales linearly with file size
6. Clean, documented public API

View File

@@ -82,15 +82,25 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream {
}
};
// Generate EcsInsertable implementation
let ecs_insertable_impl = quote! {
impl cursebreaker_parser::EcsInsertable for #struct_name {
fn insert_into_world(self, world: &mut sparsey::World, entity: sparsey::Entity) {
world.insert(entity, (self,));
}
}
};
// Generate inventory registration
let registration = quote! {
inventory::submit! {
cursebreaker_parser::ComponentRegistration {
type_id: #type_id,
class_name: #class_name,
parser: |yaml, ctx| {
#struct_name::parse(yaml, ctx)
.map(|c| Box::new(c) as Box<dyn std::any::Any>)
parse_and_insert: |yaml, ctx, world, entity| {
<#struct_name as cursebreaker_parser::EcsInsertable>::parse_and_insert(
yaml, ctx, world, entity
)
}
}
}
@@ -99,6 +109,7 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream {
// Combine everything
let expanded = quote! {
#parse_impl
#ecs_insertable_impl
#registration
};

View File

@@ -0,0 +1,146 @@
//! Example demonstrating ECS integration and selective type parsing
//!
//! This example shows:
//! 1. Custom components being automatically inserted into the ECS world
//! 2. Using the parse_with_types! macro for selective parsing
//! 3. Querying the ECS world for components
use cursebreaker_parser::{parse_with_types, ComponentContext, EcsInsertable, FileID, TypeFilter, UnityComponent};
/// Custom Unity MonoBehaviour component
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("PlaySFX")]
pub struct PlaySFX {
#[unity_field("volume")]
pub volume: f64,
#[unity_field("startTime")]
pub start_time: f64,
#[unity_field("endTime")]
pub end_time: f64,
#[unity_field("isLoop")]
pub is_loop: bool,
}
/// Another custom component
#[derive(Debug, Clone, UnityComponent)]
#[unity_class("Interactable")]
pub struct Interactable {
#[unity_field("interactionRadius")]
pub interaction_radius: f32,
#[unity_field("interactionText")]
pub interaction_text: String,
#[unity_field("canInteract")]
pub can_interact: bool,
}
fn main() {
println!("ECS Integration & Selective Parsing Example");
println!("{}", "=".repeat(60));
// Example 1: Using parse_with_types! macro
println!("\n1. Creating type filters:");
println!("{}", "-".repeat(60));
let _filter_all = TypeFilter::parse_all();
println!("✓ Filter that parses ALL types");
let filter_selective = parse_with_types! {
unity_types(Transform, Camera),
custom_types(PlaySFX)
};
println!("✓ Filter for Transform, Camera, and PlaySFX only");
let filter_custom_only = parse_with_types! {
custom_types(PlaySFX, Interactable)
};
println!("✓ Filter for PlaySFX and Interactable only (no Unity types)");
// Example 2: Demonstrating ECS insertion
println!("\n2. ECS Integration:");
println!("{}", "-".repeat(60));
// Simulate parsing a PlaySFX component
let yaml_str = r#"
volume: 0.8
startTime: 0.0
endTime: 5.0
isLoop: 0
"#;
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let mapping = yaml.as_mapping().unwrap();
let ctx = ComponentContext {
type_id: 114,
file_id: FileID::from_i64(12345),
class_name: "PlaySFX",
entity: None,
linking_ctx: None,
yaml: mapping,
};
// Parse the component
if let Some(play_sfx) = PlaySFX::parse(mapping, &ctx) {
println!("✓ Parsed PlaySFX component:");
println!(" - volume: {}", play_sfx.volume);
println!(" - start_time: {}", play_sfx.start_time);
println!(" - end_time: {}", play_sfx.end_time);
println!(" - is_loop: {}", play_sfx.is_loop);
// Create a minimal ECS world to demonstrate insertion
use sparsey::World;
let mut world = World::builder().register::<PlaySFX>().build();
let entity = world.create(());
println!("\n✓ Created ECS entity: {:?}", entity);
// Insert the component into the world
play_sfx.clone().insert_into_world(&mut world, entity);
println!("✓ Inserted PlaySFX component into ECS world");
// Query it back
{
let view = world.borrow::<PlaySFX>();
if let Some(component) = view.get(entity) {
println!("✓ Successfully queried component from ECS:");
println!(" - volume: {}", component.volume);
}
}
}
// Example 3: Type filter usage
println!("\n3. Type Filter Behavior:");
println!("{}", "-".repeat(60));
println!("Filter checks:");
println!(" Transform in selective filter: {}", filter_selective.should_parse_unity("Transform"));
println!(" Camera in selective filter: {}", filter_selective.should_parse_unity("Camera"));
println!(" Light in selective filter: {}", filter_selective.should_parse_unity("Light"));
println!(" PlaySFX in selective filter: {}", filter_selective.should_parse_custom("PlaySFX"));
println!(" Interactable in selective filter: {}", filter_selective.should_parse_custom("Interactable"));
println!("\n PlaySFX in custom-only filter: {}", filter_custom_only.should_parse_custom("PlaySFX"));
println!(" Transform in custom-only filter: {}", filter_custom_only.should_parse_unity("Transform"));
// Example 4: Benefits of selective parsing
println!("\n4. Benefits of Selective Parsing:");
println!("{}", "-".repeat(60));
println!("When parsing a large Unity project:");
println!(" • Parse ALL types: Parse everything (default)");
println!(" • Parse specific types: Faster parsing & less memory");
println!(" • Parse only what you need for your tool/analysis");
println!("\nExample use cases:");
println!(" • Animation tool: Only parse Animator, AnimationClip");
println!(" • Audio tool: Only parse AudioSource, PlaySFX");
println!(" • Transform analyzer: Only parse Transform, RectTransform");
println!();
println!("{}", "=".repeat(60));
println!("Complete! Custom components now work with ECS!");
println!("{}", "=".repeat(60));
}

View File

@@ -3,7 +3,7 @@
use crate::model::RawDocument;
use crate::types::{
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
RectTransform, Transform, UnityComponent,
RectTransform, Transform, TypeFilter, UnityComponent,
};
use crate::{Error, Result};
use sparsey::{Entity, World};
@@ -42,8 +42,9 @@ pub fn build_world_from_documents(
}
// PASS 2: Attach components to entities
let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
attach_component(&mut world, doc, &linking_ctx)?;
attach_component(&mut world, doc, &linking_ctx, &type_filter)?;
}
// PASS 3: Execute all deferred linking callbacks
@@ -90,8 +91,9 @@ pub fn build_world_from_documents_into(
}
// PASS 2: Attach components to entities
let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
attach_component(world, doc, &linking_ctx)?;
attach_component(world, doc, &linking_ctx, &type_filter)?;
}
// PASS 3: Execute all deferred linking callbacks
@@ -132,6 +134,7 @@ fn attach_component(
world: &mut World,
doc: &RawDocument,
linking_ctx: &RefCell<LinkingContext>,
type_filter: &TypeFilter,
) -> Result<()> {
let yaml = doc
.as_mapping()
@@ -168,6 +171,16 @@ fn attach_component(
yaml,
};
// Check type filter to see if we should parse this component
let is_custom = doc.class_name.as_str() != "Transform"
&& doc.class_name.as_str() != "RectTransform"
&& doc.class_name.as_str() != "PrefabInstance";
if !type_filter.should_parse(&doc.class_name, is_custom) {
// Skip this component type based on filter
return Ok(());
}
// Dispatch to appropriate component parser
match doc.class_name.as_str() {
"Transform" => {
@@ -195,26 +208,10 @@ fn attach_component(
for reg in inventory::iter::<crate::types::ComponentRegistration> {
if reg.class_name == doc.class_name.as_str() {
found_custom = true;
// Try to parse the component
if let Some(_boxed_component) = (reg.parser)(yaml, &ctx) {
eprintln!(
"Info: Custom component '{}' parsed successfully via #[derive(UnityComponent)]",
doc.class_name
);
eprintln!(
"Note: ECS integration for custom components is not yet fully implemented."
);
eprintln!(
" Component data was parsed but not inserted into the ECS world."
);
eprintln!(
" To use this data, access it directly from the parsed documents."
);
// TODO: Future enhancement - add dynamic component insertion support
// This would require either:
// 1. A type-erased component enum wrapper
// 2. Component trait objects in the ECS
// 3. User-defined registration with downcasting logic
// Parse and insert the component into the ECS world
if (reg.parse_and_insert)(yaml, &ctx, world, entity) {
// Successfully parsed and inserted
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
}
break;
}

View File

@@ -27,6 +27,7 @@
// Public modules
pub mod ecs;
pub mod error;
pub mod macros;
pub mod model;
pub mod parser;
// TODO: Update project module to work with new UnityFile enum architecture
@@ -42,10 +43,10 @@ pub use parser::{meta::MetaFile, parse_unity_file};
// pub use project::UnityProject;
pub use property::PropertyValue;
pub use types::{
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, ExternalRef,
FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent,
PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, UnityComponent,
UnityReference, Vector2, Vector3, yaml_helpers,
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable,
ExternalRef, FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent,
PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, TypeFilter,
UnityComponent, UnityReference, Vector2, Vector3, yaml_helpers,
};
// Re-export the derive macro from the macro crate

View File

@@ -0,0 +1,111 @@
//! Declarative macros for convenient API usage
/// Create a TypeFilter with specific Unity and custom types
///
/// # Syntax
/// ```ignore
/// parse_with_types! {
/// unity_types(Transform, Camera, Light),
/// custom_types(PlaySFX, Interact)
/// }
/// ```
///
/// You can omit either section:
/// ```ignore
/// // Only Unity types
/// parse_with_types! {
/// unity_types(Transform, Camera)
/// }
///
/// // Only custom types
/// parse_with_types! {
/// custom_types(PlaySFX)
/// }
/// ```
#[macro_export]
macro_rules! parse_with_types {
// Full syntax with both Unity and custom types
(unity_types($($unity:ident),+ $(,)?), custom_types($($custom:ident),+ $(,)?)) => {
$crate::TypeFilter::new(
vec![$(stringify!($unity)),+],
vec![$(stringify!($custom)),+]
)
};
// Only Unity types
(unity_types($($unity:ident),+ $(,)?)) => {
$crate::TypeFilter::unity_only(
vec![$(stringify!($unity)),+]
)
};
// Only custom types
(custom_types($($custom:ident),+ $(,)?)) => {
$crate::TypeFilter::custom_only(
vec![$(stringify!($custom)),+]
)
};
// Alternative order: custom_types first
(custom_types($($custom:ident),+ $(,)?), unity_types($($unity:ident),+ $(,)?)) => {
$crate::TypeFilter::new(
vec![$(stringify!($unity)),+],
vec![$(stringify!($custom)),+]
)
};
}
#[cfg(test)]
mod tests {
use crate::TypeFilter;
#[test]
fn test_parse_with_types_macro() {
let filter = parse_with_types! {
unity_types(Transform, Camera, Light),
custom_types(PlaySFX, Interact)
};
assert!(filter.should_parse_unity("Transform"));
assert!(filter.should_parse_unity("Camera"));
assert!(filter.should_parse_unity("Light"));
assert!(!filter.should_parse_unity("AudioSource"));
assert!(filter.should_parse_custom("PlaySFX"));
assert!(filter.should_parse_custom("Interact"));
assert!(!filter.should_parse_custom("OtherComponent"));
}
#[test]
fn test_parse_with_types_unity_only() {
let filter = parse_with_types! {
unity_types(Transform, Camera)
};
assert!(filter.should_parse_unity("Transform"));
assert!(!filter.should_parse_unity("Light"));
assert!(!filter.should_parse_custom("PlaySFX"));
}
#[test]
fn test_parse_with_types_custom_only() {
let filter = parse_with_types! {
custom_types(PlaySFX, Interact)
};
assert!(!filter.should_parse_unity("Transform"));
assert!(filter.should_parse_custom("PlaySFX"));
assert!(filter.should_parse_custom("Interact"));
}
#[test]
fn test_parse_with_types_reversed_order() {
let filter = parse_with_types! {
custom_types(PlaySFX),
unity_types(Transform)
};
assert!(filter.should_parse_unity("Transform"));
assert!(filter.should_parse_custom("PlaySFX"));
}
}

View File

@@ -84,6 +84,29 @@ pub trait UnityComponent: Sized {
fn parse(yaml: &Mapping, ctx: &ComponentContext) -> Option<Self>;
}
/// Trait for components that can be inserted into the ECS world
///
/// This enables dynamic component insertion for both built-in and custom components.
pub trait EcsInsertable: UnityComponent {
/// Insert this component into the ECS world
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity);
/// Parse and insert in one step
fn parse_and_insert(
yaml: &Mapping,
ctx: &ComponentContext,
world: &mut sparsey::World,
entity: Entity,
) -> bool {
if let Some(component) = Self::parse(yaml, ctx) {
component.insert_into_world(world, entity);
true
} else {
false
}
}
}
/// Registration entry for custom Unity components
///
/// This is submitted via the `inventory` crate by the `#[derive(UnityComponent)]` macro
@@ -93,8 +116,8 @@ pub struct ComponentRegistration {
pub type_id: u32,
/// Unity class name (e.g., "PlaySFX")
pub class_name: &'static str,
/// Parser function that creates a boxed component from YAML
pub parser: fn(&Mapping, &ComponentContext) -> Option<Box<dyn std::any::Any>>,
/// Parser function that parses and inserts the component into the ECS world
pub parse_and_insert: fn(&Mapping, &ComponentContext, &mut sparsey::World, Entity) -> bool,
}
// Collect all component registrations submitted via the macro

View File

@@ -1,6 +1,7 @@
//! GameObject component
use crate::types::{yaml_helpers, ComponentContext, UnityComponent};
use sparsey::Entity;
/// A GameObject component
///
@@ -56,3 +57,9 @@ impl UnityComponent for GameObject {
})
}
}
impl crate::types::EcsInsertable for GameObject {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}

View File

@@ -10,12 +10,13 @@ mod ids;
mod prefab_instance;
mod reference;
mod transform;
mod type_filter;
mod type_registry;
mod values;
pub use component::{
yaml_helpers, ComponentContext, ComponentRegistration, LinkCallback, LinkingContext,
UnityComponent,
yaml_helpers, ComponentContext, ComponentRegistration, EcsInsertable, LinkCallback,
LinkingContext, UnityComponent,
};
pub use game_object::GameObject;
pub use ids::{FileID, LocalID};
@@ -24,5 +25,6 @@ pub use prefab_instance::{
};
pub use reference::UnityReference;
pub use transform::{RectTransform, Transform};
pub use type_filter::TypeFilter;
pub use type_registry::{get_class_name, get_type_id};
pub use values::{Color, ExternalRef, FileRef, Quaternion, Vector2, Vector3};

View File

@@ -337,6 +337,12 @@ impl UnityComponent for PrefabInstanceComponent {
}
}
impl crate::types::EcsInsertable for PrefabInstanceComponent {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}
/// A modification applied to a nested prefab
///
/// Unity stores modifications as changes to specific properties of objects

View File

@@ -126,6 +126,12 @@ impl UnityComponent for Transform {
}
}
impl crate::types::EcsInsertable for Transform {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}
/// A RectTransform component
///
/// RectTransform is used for UI elements and extends Transform with additional properties.
@@ -288,3 +294,9 @@ impl UnityComponent for RectTransform {
})
}
}
impl crate::types::EcsInsertable for RectTransform {
fn insert_into_world(self, world: &mut sparsey::World, entity: Entity) {
world.insert(entity, (self,));
}
}

View File

@@ -0,0 +1,161 @@
//! Type filtering for selective parsing
//!
//! This module provides functionality to selectively parse only specific Unity
//! component types, improving performance and reducing memory usage.
use std::collections::HashSet;
/// Filter for controlling which Unity types get parsed
///
/// By default, all types are parsed. Use `TypeFilter::new()` to create
/// a filter that only parses specific types.
#[derive(Debug, Clone)]
pub struct TypeFilter {
/// Set of Unity type names to parse (e.g., "Transform", "Camera")
/// If None, all types are parsed
unity_types: Option<HashSet<String>>,
/// Set of custom component names to parse (e.g., "PlaySFX")
/// If None, all custom types are parsed
custom_types: Option<HashSet<String>>,
/// Whether to parse all types (default)
parse_all: bool,
}
impl TypeFilter {
/// Create a new filter that parses ALL types (default behavior)
pub fn parse_all() -> Self {
Self {
unity_types: None,
custom_types: None,
parse_all: true,
}
}
/// Create a new filter with specific Unity and custom types
///
/// # Example
/// ```
/// use cursebreaker_parser::TypeFilter;
///
/// let filter = TypeFilter::new(
/// vec!["Transform", "Camera", "Light"],
/// vec!["PlaySFX", "Interact"]
/// );
/// ```
pub fn new<S1, S2>(unity_types: Vec<S1>, custom_types: Vec<S2>) -> Self
where
S1: Into<String>,
S2: Into<String>,
{
Self {
unity_types: Some(unity_types.into_iter().map(|s| s.into()).collect()),
custom_types: Some(custom_types.into_iter().map(|s| s.into()).collect()),
parse_all: false,
}
}
/// Create a filter that only parses specific Unity types (no custom types)
pub fn unity_only<S: Into<String>>(types: Vec<S>) -> Self {
Self {
unity_types: Some(types.into_iter().map(|s| s.into()).collect()),
custom_types: Some(HashSet::new()),
parse_all: false,
}
}
/// Create a filter that only parses specific custom types (no Unity types)
pub fn custom_only<S: Into<String>>(types: Vec<S>) -> Self {
Self {
unity_types: Some(HashSet::new()),
custom_types: Some(types.into_iter().map(|s| s.into()).collect()),
parse_all: false,
}
}
/// Check if a Unity type should be parsed
pub fn should_parse_unity(&self, type_name: &str) -> bool {
if self.parse_all {
return true;
}
match &self.unity_types {
Some(types) => types.contains(type_name),
None => true, // If not specified, parse all
}
}
/// Check if a custom type should be parsed
pub fn should_parse_custom(&self, type_name: &str) -> bool {
if self.parse_all {
return true;
}
match &self.custom_types {
Some(types) => types.contains(type_name),
None => true, // If not specified, parse all
}
}
/// Check if any type should be parsed
pub fn should_parse(&self, type_name: &str, is_custom: bool) -> bool {
if is_custom {
self.should_parse_custom(type_name)
} else {
self.should_parse_unity(type_name)
}
}
}
impl Default for TypeFilter {
fn default() -> Self {
Self::parse_all()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_all() {
let filter = TypeFilter::parse_all();
assert!(filter.should_parse_unity("Transform"));
assert!(filter.should_parse_unity("Camera"));
assert!(filter.should_parse_custom("PlaySFX"));
}
#[test]
fn test_specific_types() {
let filter = TypeFilter::new(
vec!["Transform", "Camera"],
vec!["PlaySFX"]
);
assert!(filter.should_parse_unity("Transform"));
assert!(filter.should_parse_unity("Camera"));
assert!(!filter.should_parse_unity("Light"));
assert!(filter.should_parse_custom("PlaySFX"));
assert!(!filter.should_parse_custom("Interact"));
}
#[test]
fn test_unity_only() {
let filter = TypeFilter::unity_only(vec!["Transform"]);
assert!(filter.should_parse_unity("Transform"));
assert!(!filter.should_parse_unity("Camera"));
assert!(!filter.should_parse_custom("PlaySFX"));
}
#[test]
fn test_custom_only() {
let filter = TypeFilter::custom_only(vec!["PlaySFX"]);
assert!(!filter.should_parse_unity("Transform"));
assert!(filter.should_parse_custom("PlaySFX"));
assert!(!filter.should_parse_custom("Interact"));
}
}