diff --git a/GEMINI.md b/GEMINI.md index 1a252c5..a12da6a 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -179,7 +179,37 @@ In the Punc architecture, filters are automatically synthesized, strongly-typed * **Inherited Properties**: Filters automatically inherit all valid database columns from their base type schema, immediately converting them to their respective `.condition` schemas. * **Relational Proxies**: If a table has a foreign key to another table, the filter automatically generates a proxy property pointing to the related entity's filter (e.g., the `person` filter automatically gains an `organization` property that points to `organization.filter`), allowing infinitely deep nested queries natively. * **Logical Operators (`$and`, `$or`)**: Every filter automatically includes `$and` and `$or` arrays, which recursively accept the exact same filter schema, allowing complex logical grouping. - * **Ad-Hoc Extensions (`ad_hoc`)**: Fields stored purely in JSONB bubbles that lack formal database columns can still be queried using the `ad_hoc` object, which passes standard, unvalidated string conditions. + * Ad-Hoc Extensions (`ad_hoc`)**: Fields stored purely in JSONB bubbles that lack formal database columns can still be queried using the `ad_hoc` object, which passes standard, unvalidated string conditions. + +### Trait-Based Schema Composition (`traits` & `include`) +Traits are reusable, non-generating schema fragments used to share properties and relationships horizontally across multiple schemas. They do not generate separate Go/Dart classes. + +* **Traits Namespace**: Defined in a sibling `"traits"` block next to `"schemas"` inside table comments: + ```json + "traits": { + "emailable": { + "properties": { + "email_addresses": { + "type": "array", + "items": { "type": "contact", "properties": { "target": { "type": "email_address" } } } + } + } + } + } + ``` +* **Include Keyword**: Schemas or traits can use the `"include"` array to compose traits or other schemas: + ```json + "full.person": { + "type": "person", + "include": ["contactable", "owner"] + } + ``` +* **Resolution and Merging**: During `Database::new()`, includes are resolved and merged at the raw JSON level: + * **`properties` / `patternProperties`**: Map keys from the host schema override/shadow included traits. + * **`required` / `display`**: Lists are merged and deduped. + * **`dependencies`**: Merged by combining and deduping lists. + * **Scalars / Arrays / Items**: Host definitions completely override included traits. + * The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization. --- diff --git a/fixtures/traits.json b/fixtures/traits.json new file mode 100644 index 0000000..d44cd4f --- /dev/null +++ b/fixtures/traits.json @@ -0,0 +1,191 @@ +[ + { + "description": "Granular trait composition and list merging", + "database": { + "types": [ + { + "name": "person", + "schemas": { + "full.person": { + "type": "object", + "include": ["emailable", "phonable"], + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + }, + "traits": { + "emailable": { + "properties": { + "email": { "type": "string" } + }, + "required": ["email"], + "display": ["email"] + }, + "phonable": { + "properties": { + "phone": { "type": "string" } + }, + "required": ["phone"], + "display": ["phone"] + } + } + } + ] + }, + "tests": [ + { + "description": "valid person with name, email, and phone passes", + "schema_id": "full.person", + "action": "validate", + "data": { + "name": "Jane Doe", + "email": "jane@example.com", + "phone": "555-1234" + }, + "expect": { + "success": true + } + }, + { + "description": "missing email fails validation", + "schema_id": "full.person", + "action": "validate", + "data": { + "name": "Jane Doe", + "phone": "555-1234" + }, + "expect": { + "success": false, + "errors": [ + { + "code": "REQUIRED_FIELD_MISSING", + "details": { + "path": "email" + } + } + ] + } + } + ] + }, + { + "description": "Local property shadowing", + "database": { + "types": [ + { + "name": "person", + "schemas": { + "full.person": { + "type": "object", + "include": ["emailable"], + "properties": { + "email": { + "type": "string", + "maxLength": 5 + } + } + } + }, + "traits": { + "emailable": { + "properties": { + "email": { "type": "string" } + } + } + } + } + ] + }, + "tests": [ + { + "description": "local maxLength overrides trait properties", + "schema_id": "full.person", + "action": "validate", + "data": { + "email": "longerthanfive@example.com" + }, + "expect": { + "success": false, + "errors": [ + { + "code": "MAX_LENGTH_VIOLATED", + "details": { + "path": "email" + } + } + ] + } + } + ] + }, + { + "description": "Missing trait compiler error", + "database": { + "types": [ + { + "name": "person", + "schemas": { + "full.person": { + "type": "object", + "include": ["nonexistent_trait"] + } + } + } + ] + }, + "tests": [ + { + "description": "emits TRAIT_NOT_FOUND compile error", + "action": "compile", + "expect": { + "success": false, + "errors": [ + { + "code": "TRAIT_NOT_FOUND" + } + ] + } + } + ] + }, + { + "description": "Circular inclusion compiler error", + "database": { + "types": [ + { + "name": "person", + "schemas": { + "full.person": { + "type": "object", + "include": ["trait_a"] + } + }, + "traits": { + "trait_a": { + "include": ["trait_b"] + }, + "trait_b": { + "include": ["trait_a"] + } + } + } + ] + }, + "tests": [ + { + "description": "emits CIRCULAR_INCLUDE_DETECTED compile error", + "action": "compile", + "expect": { + "success": false, + "errors": [ + { + "code": "CIRCULAR_INCLUDE_DETECTED" + } + ] + } + } + ] + } +] diff --git a/src/database/compose/mod.rs b/src/database/compose/mod.rs new file mode 100644 index 0000000..396f0d0 --- /dev/null +++ b/src/database/compose/mod.rs @@ -0,0 +1,300 @@ +use std::collections::{HashMap, HashSet}; +use serde_json::Value; + +pub fn compose(val: &mut Value, errors: &mut Vec) -> Result<(), String> { + let mut traits = HashMap::new(); + let mut schemas = HashMap::new(); + + // 1. Gather all traits and schemas from enums, types, and puncs arrays + let arrays = ["enums", "types", "puncs"]; + for arr_name in &arrays { + if let Some(arr) = val.get(arr_name).and_then(|v| v.as_array()) { + for item in arr { + if let Some(item_traits) = item.get("traits").and_then(|v| v.as_object()) { + for (name, trait_val) in item_traits { + traits.insert(name.clone(), trait_val.clone()); + } + } + if let Some(item_schemas) = item.get("schemas").and_then(|v| v.as_object()) { + for (name, schema_val) in item_schemas { + schemas.insert(name.clone(), schema_val.clone()); + } + } + } + } + } + + // 2. Resolve inclusions recursively in all schema objects + for arr_name in &arrays { + if let Some(arr) = val.get_mut(arr_name).and_then(|v| v.as_array_mut()) { + for item in arr { + if let Some(item_schemas) = item.get_mut("schemas").and_then(|v| v.as_object_mut()) { + for (schema_id, schema_val) in item_schemas { + let mut visited = HashSet::new(); + resolve_in_place( + schema_val, + &traits, + &schemas, + errors, + schema_id, + schema_id, + &mut visited, + ); + } + } + } + } + } + + // 3. Strip the "traits" block from each item in enums, types, puncs so it doesn't serialize + for arr_name in &arrays { + if let Some(arr) = val.get_mut(arr_name).and_then(|v| v.as_array_mut()) { + for item in arr { + if let Some(obj) = item.as_object_mut() { + obj.remove("traits"); + } + } + } + } + + Ok(()) +} + +fn resolve_in_place( + current: &mut Value, + traits: &HashMap, + schemas: &HashMap, + errors: &mut Vec, + schema_id: &str, + path: &str, + visited: &mut HashSet, +) { + if !current.is_object() { + return; + } + + let include_opt = current.as_object_mut().and_then(|obj| obj.remove("include")); + if let Some(include_val) = include_opt { + if let Some(include_arr) = include_val.as_array() { + let mut merged_props = serde_json::Map::new(); + let mut merged_required = HashSet::new(); + let mut merged_display = HashSet::new(); + let mut merged_dependencies = serde_json::Map::new(); + let mut merged_pattern_props = serde_json::Map::new(); + + // Read current values first to let host override included properties + if let Some(req) = current.get("required").and_then(|v| v.as_array()) { + for r in req { + if let Some(s) = r.as_str() { + merged_required.insert(s.to_string()); + } + } + } + if let Some(disp) = current.get("display").and_then(|v| v.as_array()) { + for d in disp { + if let Some(s) = d.as_str() { + merged_display.insert(s.to_string()); + } + } + } + if let Some(deps) = current.get("dependencies").and_then(|v| v.as_object()) { + for (k, v) in deps { + merged_dependencies.insert(k.clone(), v.clone()); + } + } + if let Some(pat_props) = current.get("patternProperties").and_then(|v| v.as_object()) { + for (k, v) in pat_props { + merged_pattern_props.insert(k.clone(), v.clone()); + } + } + if let Some(props) = current.get("properties").and_then(|v| v.as_object()) { + for (k, v) in props { + merged_props.insert(k.clone(), v.clone()); + } + } + + for inc in include_arr { + if let Some(inc_name) = inc.as_str() { + if visited.contains(inc_name) { + errors.push(crate::drop::Error { + code: "CIRCULAR_INCLUDE_DETECTED".to_string(), + message: format!("Circular inclusion detected for '{}'", inc_name), + details: crate::drop::ErrorDetails { + schema: Some(schema_id.to_string()), + path: Some(path.to_string()), + ..Default::default() + }, + }); + continue; + } + + let target_opt = traits.get(inc_name).or_else(|| schemas.get(inc_name)); + if let Some(target_val) = target_opt { + let mut resolved_target = target_val.clone(); + visited.insert(inc_name.to_string()); + resolve_in_place( + &mut resolved_target, + traits, + schemas, + errors, + schema_id, + &format!("{}/include/{}", path, inc_name), + visited, + ); + visited.remove(inc_name); + + // Merge properties (host overrides trait) + if let Some(target_props) = resolved_target.get("properties").and_then(|v| v.as_object()) { + for (k, v) in target_props { + if !merged_props.contains_key(k) { + merged_props.insert(k.clone(), v.clone()); + } + } + } + + // Merge patternProperties (host overrides trait) + if let Some(target_pat_props) = resolved_target.get("patternProperties").and_then(|v| v.as_object()) { + for (k, v) in target_pat_props { + if !merged_pattern_props.contains_key(k) { + merged_pattern_props.insert(k.clone(), v.clone()); + } + } + } + + // Merge required + if let Some(target_req) = resolved_target.get("required").and_then(|v| v.as_array()) { + for r in target_req { + if let Some(s) = r.as_str() { + merged_required.insert(s.to_string()); + } + } + } + + // Merge display + if let Some(target_disp) = resolved_target.get("display").and_then(|v| v.as_array()) { + for d in target_disp { + if let Some(s) = d.as_str() { + merged_display.insert(s.to_string()); + } + } + } + + // Merge dependencies + if let Some(target_deps) = resolved_target.get("dependencies").and_then(|v| v.as_object()) { + for (dep_prop, dep_val) in target_deps { + if let Some(existing_val) = merged_dependencies.get_mut(dep_prop) { + if let (Some(arr_existing), Some(arr_target)) = (existing_val.as_array_mut(), dep_val.as_array()) { + let mut set: HashSet = arr_existing.iter().filter_map(|x| x.as_str().map(String::from)).collect(); + for x in arr_target { + if let Some(s) = x.as_str() { + if set.insert(s.to_string()) { + arr_existing.push(Value::String(s.to_string())); + } + } + } + } + } else { + merged_dependencies.insert(dep_prop.clone(), dep_val.clone()); + } + } + } + + // Inherit other non-merged schemas/scalars if not defined in host (type, items, cases, family, format, etc.) + if let Some(obj) = current.as_object_mut() { + for (k, v) in resolved_target.as_object().unwrap() { + if k != "properties" && k != "patternProperties" && k != "required" && k != "display" && k != "dependencies" && k != "include" { + if !obj.contains_key(k) { + obj.insert(k.clone(), v.clone()); + } + } + } + } + } else { + errors.push(crate::drop::Error { + code: "TRAIT_NOT_FOUND".to_string(), + message: format!("Trait or schema '{}' not found for inclusion", inc_name), + details: crate::drop::ErrorDetails { + schema: Some(schema_id.to_string()), + path: Some(path.to_string()), + ..Default::default() + }, + }); + } + } + } + + if let Some(obj) = current.as_object_mut() { + if !merged_props.is_empty() { + obj.insert("properties".to_string(), Value::Object(merged_props)); + } + if !merged_pattern_props.is_empty() { + obj.insert("patternProperties".to_string(), Value::Object(merged_pattern_props)); + } + if !merged_required.is_empty() { + let mut req_vec: Vec = merged_required.into_iter().map(Value::String).collect(); + req_vec.sort_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap())); + obj.insert("required".to_string(), Value::Array(req_vec)); + } + if !merged_display.is_empty() { + let mut disp_vec: Vec = merged_display.into_iter().map(Value::String).collect(); + disp_vec.sort_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap())); + obj.insert("display".to_string(), Value::Array(disp_vec)); + } + if !merged_dependencies.is_empty() { + obj.insert("dependencies".to_string(), Value::Object(merged_dependencies)); + } + } + } + } + + // Recursively process children + if let Some(obj) = current.as_object_mut() { + if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) { + for (k, v) in props { + resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/{}", path, k), visited); + } + } + if let Some(pat_props) = obj.get_mut("patternProperties").and_then(|v| v.as_object_mut()) { + for (k, v) in pat_props { + resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/{}", path, k), visited); + } + } + if let Some(items) = obj.get_mut("items") { + resolve_in_place(items, traits, schemas, errors, schema_id, &format!("{}/items", path), visited); + } + if let Some(prefix_items) = obj.get_mut("prefixItems").and_then(|v| v.as_array_mut()) { + for (i, v) in prefix_items.iter_mut().enumerate() { + resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/prefixItems/{}", path, i), visited); + } + } + if let Some(additional_props) = obj.get_mut("additionalProperties") { + resolve_in_place(additional_props, traits, schemas, errors, schema_id, &format!("{}/additionalProperties", path), visited); + } + if let Some(one_of) = obj.get_mut("oneOf").and_then(|v| v.as_array_mut()) { + for (i, v) in one_of.iter_mut().enumerate() { + resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/oneOf/{}", path, i), visited); + } + } + if let Some(contains) = obj.get_mut("contains") { + resolve_in_place(contains, traits, schemas, errors, schema_id, &format!("{}/contains", path), visited); + } + if let Some(not) = obj.get_mut("not") { + resolve_in_place(not, traits, schemas, errors, schema_id, &format!("{}/not", path), visited); + } + if let Some(cases) = obj.get_mut("cases").and_then(|v| v.as_array_mut()) { + for (i, c_val) in cases.iter_mut().enumerate() { + if let Some(c_obj) = c_val.as_object_mut() { + if let Some(when) = c_obj.get_mut("when") { + resolve_in_place(when, traits, schemas, errors, schema_id, &format!("{}/cases/{}/when", path, i), visited); + } + if let Some(then) = c_obj.get_mut("then") { + resolve_in_place(then, traits, schemas, errors, schema_id, &format!("{}/cases/{}/then", path, i), visited); + } + if let Some(else_) = c_obj.get_mut("else") { + resolve_in_place(else_, traits, schemas, errors, schema_id, &format!("{}/cases/{}/else", path, i), visited); + } + } + } + } + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index f1a6653..e36ff1e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,5 +1,6 @@ pub mod action; pub mod compile; +pub mod compose; pub mod edge; pub mod r#enum; pub mod executors; @@ -41,7 +42,7 @@ pub struct Database { } impl Database { - pub fn new(val: &serde_json::Value) -> (Self, crate::drop::Drop) { + pub fn new(mut val: serde_json::Value) -> (Self, crate::drop::Drop) { let mut db = Self { enums: IndexMap::new(), types: IndexMap::new(), @@ -56,101 +57,115 @@ impl Database { let mut errors = Vec::new(); - if let Some(arr) = val.get("enums").and_then(|v| v.as_array()) { - for item in arr { - match serde_json::from_value::(item.clone()) { - Ok(def) => { - db.enums.insert(def.name.clone(), def); - } - Err(e) => { - let name = item - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - errors.push(crate::drop::Error { - code: "DATABASE_ENUM_PARSE_FAILED".to_string(), - message: format!("Failed to parse database enum '{}': {}", name, e), - details: crate::drop::ErrorDetails { - context: Some(serde_json::json!(name)), - ..Default::default() - }, - }); - } - } - } + if let Err(e) = compose::compose(&mut val, &mut errors) { + errors.push(crate::drop::Error { + code: "COMPOSE_FAILED".to_string(), + message: format!("Fatal error during trait composition: {}", e), + details: crate::drop::ErrorDetails::default(), + }); } - if let Some(arr) = val.get("types").and_then(|v| v.as_array()) { - for item in arr { - match serde_json::from_value::(item.clone()) { - Ok(def) => { - db.types.insert(def.name.clone(), def); - } - Err(e) => { - let name = item - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - errors.push(crate::drop::Error { - code: "DATABASE_TYPE_PARSE_FAILED".to_string(), - message: format!("Failed to parse database type '{}': {}", name, e), - details: crate::drop::ErrorDetails { - context: Some(serde_json::json!(name)), - ..Default::default() - }, - }); - } - } - } - } - - if let Some(arr) = val.get("relations").and_then(|v| v.as_array()) { - for item in arr { - match serde_json::from_value::(item.clone()) { - Ok(def) => { - if db.types.contains_key(&def.source_type) - && db.types.contains_key(&def.destination_type) - { - db.relations.insert(def.constraint.clone(), def); + if let serde_json::Value::Object(mut map) = val { + if let Some(serde_json::Value::Array(arr)) = map.remove("enums") { + for item in arr { + let name = item + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + match serde_json::from_value::(item) { + Ok(def) => { + db.enums.insert(def.name.clone(), def); + } + Err(e) => { + errors.push(crate::drop::Error { + code: "DATABASE_ENUM_PARSE_FAILED".to_string(), + message: format!("Failed to parse database enum '{}': {}", name, e), + details: crate::drop::ErrorDetails { + context: Some(serde_json::json!(name)), + ..Default::default() + }, + }); } } - Err(e) => { - let constraint = item - .get("constraint") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - errors.push(crate::drop::Error { - code: "DATABASE_RELATION_PARSE_FAILED".to_string(), - message: format!("Failed to parse database relation '{}': {}", constraint, e), - details: crate::drop::ErrorDetails { - context: Some(serde_json::json!(constraint)), - ..Default::default() - }, - }); + } + } + + if let Some(serde_json::Value::Array(arr)) = map.remove("types") { + for item in arr { + let name = item + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + match serde_json::from_value::(item) { + Ok(def) => { + db.types.insert(def.name.clone(), def); + } + Err(e) => { + errors.push(crate::drop::Error { + code: "DATABASE_TYPE_PARSE_FAILED".to_string(), + message: format!("Failed to parse database type '{}': {}", name, e), + details: crate::drop::ErrorDetails { + context: Some(serde_json::json!(name)), + ..Default::default() + }, + }); + } } } } - } - if let Some(arr) = val.get("puncs").and_then(|v| v.as_array()) { - for item in arr { - match serde_json::from_value::(item.clone()) { - Ok(def) => { - db.puncs.insert(def.name.clone(), def); + if let Some(serde_json::Value::Array(arr)) = map.remove("relations") { + for item in arr { + let constraint = item + .get("constraint") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + match serde_json::from_value::(item) { + Ok(def) => { + if db.types.contains_key(&def.source_type) + && db.types.contains_key(&def.destination_type) + { + db.relations.insert(def.constraint.clone(), def); + } + } + Err(e) => { + errors.push(crate::drop::Error { + code: "DATABASE_RELATION_PARSE_FAILED".to_string(), + message: format!("Failed to parse database relation '{}': {}", constraint, e), + details: crate::drop::ErrorDetails { + context: Some(serde_json::json!(constraint)), + ..Default::default() + }, + }); + } } - Err(e) => { - let name = item - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - errors.push(crate::drop::Error { - code: "DATABASE_PUNC_PARSE_FAILED".to_string(), - message: format!("Failed to parse database punc '{}': {}", name, e), - details: crate::drop::ErrorDetails { - context: Some(serde_json::json!(name)), - ..Default::default() - }, - }); + } + } + + if let Some(serde_json::Value::Array(arr)) = map.remove("puncs") { + for item in arr { + let name = item + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + match serde_json::from_value::(item) { + Ok(def) => { + db.puncs.insert(def.name.clone(), def); + } + Err(e) => { + errors.push(crate::drop::Error { + code: "DATABASE_PUNC_PARSE_FAILED".to_string(), + message: format!("Failed to parse database punc '{}': {}", name, e), + details: crate::drop::ErrorDetails { + context: Some(serde_json::json!(name)), + ..Default::default() + }, + }); + } } } } diff --git a/src/jspg.rs b/src/jspg.rs index 59a467a..29f1d4f 100644 --- a/src/jspg.rs +++ b/src/jspg.rs @@ -12,7 +12,7 @@ pub struct Jspg { } impl Jspg { - pub fn new(database_val: &serde_json::Value) -> (Self, crate::drop::Drop) { + pub fn new(database_val: serde_json::Value) -> (Self, crate::drop::Drop) { let (database_instance, drop) = Database::new(database_val); let database = Arc::new(database_instance); let validator = Validator::new(database.clone()); diff --git a/src/lib.rs b/src/lib.rs index af8e47b..bf0d0d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ fn jspg_failure() -> JsonB { #[cfg_attr(not(test), pg_extern(strict))] pub fn jspg_setup(database: Json) -> Json { - let (new_jspg, drop) = crate::jspg::Jspg::new(&database.0); + let (new_jspg, drop) = crate::jspg::Jspg::new(database.0); let new_arc = Arc::new(new_jspg); // 3. ATOMIC SWAP diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 244bec2..8034435 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -2141,6 +2141,36 @@ fn test_items_15_2() { crate::tests::runner::run_test_case(&path, 15, 2).unwrap(); } +#[test] +fn test_traits_0_0() { + let path = format!("{}/fixtures/traits.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 0).unwrap(); +} + +#[test] +fn test_traits_0_1() { + let path = format!("{}/fixtures/traits.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 1).unwrap(); +} + +#[test] +fn test_traits_1_0() { + let path = format!("{}/fixtures/traits.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 1, 0).unwrap(); +} + +#[test] +fn test_traits_2_0() { + let path = format!("{}/fixtures/traits.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 2, 0).unwrap(); +} + +#[test] +fn test_traits_3_0() { + let path = format!("{}/fixtures/traits.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 3, 0).unwrap(); +} + #[test] fn test_enum_0_0() { let path = format!("{}/fixtures/enum.json", env!("CARGO_MANIFEST_DIR")); diff --git a/src/tests/runner.rs b/src/tests/runner.rs index 19b74e3..119e9f2 100644 --- a/src/tests/runner.rs +++ b/src/tests/runner.rs @@ -42,7 +42,7 @@ fn get_cached_file(path: &str) -> CompiledSuite { let mut compiled_suites = Vec::new(); for suite in suites { - let (db, drop) = crate::database::Database::new(&suite.database); + let (db, drop) = crate::database::Database::new(suite.database.clone()); let compiled_db = if drop.errors.is_empty() { Ok(Arc::new(db)) } else {