Skip to main content

jpx_engine/
lib.rs

1//! # jpx-engine
2//!
3//! Protocol-agnostic JMESPath query engine with 400+ functions.
4//!
5//! This crate provides the core "brain" of jpx - everything you can do with JMESPath
6//! beyond basic compile and evaluate. It's designed to be transport-agnostic, allowing
7//! the CLI (`jpx`), MCP server (`jpx-mcp`), or any future REST/gRPC adapters to be
8//! thin wrappers over this engine.
9//!
10//! ## Features
11//!
12//! | Category | Description |
13//! |----------|-------------|
14//! | **Evaluation** | Single, batch, and string-based evaluation with validation |
15//! | **Introspection** | List functions, search by keyword, describe, find similar |
16//! | **Discovery** | Cross-server tool discovery with BM25 search indexing |
17//! | **Query Store** | Named queries for session-scoped reuse |
18//! | **Configuration** | Declarative `jpx.toml` config with layered discovery and merge |
19//! | **JSON Utilities** | Format, diff, patch, merge, stats, paths, keys |
20//! | **Arrow** | Apache Arrow conversion (optional, via `arrow` feature) |
21//!
22//! ## Cargo Features
23//!
24//! - **`arrow`** - Enables Apache Arrow support for columnar data conversion.
25//!   This adds the `arrow` module with functions to convert between Arrow
26//!   RecordBatches and JSON Values. Used by the CLI for Parquet I/O.
27//! - **`let-expr`** - Enables `let` expression support (variable bindings in
28//!   JMESPath expressions). Forwarded from jpx-core. Enabled by default.
29//! - **`schema`** - Derives `JsonSchema` on discovery types for JSON Schema
30//!   generation. Used by the MCP server for tool schemas.
31//!
32//! ## Quick Start
33//!
34//! ```rust
35//! use jpx_engine::JpxEngine;
36//! use serde_json::json;
37//!
38//! let engine = JpxEngine::new();
39//!
40//! // Evaluate a JMESPath expression
41//! let result = engine.evaluate("users[*].name", &json!({
42//!     "users": [{"name": "alice"}, {"name": "bob"}]
43//! })).unwrap();
44//! assert_eq!(result, json!(["alice", "bob"]));
45//! ```
46//!
47//! ## Evaluation
48//!
49//! The engine supports multiple evaluation modes:
50//!
51//! ```rust
52//! use jpx_engine::JpxEngine;
53//! use serde_json::json;
54//!
55//! let engine = JpxEngine::new();
56//!
57//! // From parsed JSON
58//! let data = json!({"items": [1, 2, 3]});
59//! let result = engine.evaluate("length(items)", &data).unwrap();
60//! assert_eq!(result, json!(3));
61//!
62//! // From JSON string
63//! let result = engine.evaluate_str("length(@)", r#"[1, 2, 3]"#).unwrap();
64//! assert_eq!(result, json!(3));
65//!
66//! // Batch evaluation (multiple expressions, same input)
67//! let exprs = vec!["a".to_string(), "b".to_string()];
68//! let batch = engine.batch_evaluate(&exprs, &json!({"a": 1, "b": 2}));
69//! assert_eq!(batch.results[0].result, Some(json!(1)));
70//!
71//! // Validation without evaluation
72//! let valid = engine.validate("users[*].name");
73//! assert!(valid.valid);
74//! ```
75//!
76//! ## Function Introspection
77//!
78//! Discover and explore the 400+ available functions:
79//!
80//! ```rust
81//! use jpx_engine::JpxEngine;
82//!
83//! let engine = JpxEngine::new();
84//!
85//! // List all categories
86//! let categories = engine.categories();
87//! assert!(categories.contains(&"String".to_string()));
88//!
89//! // List functions in a category
90//! let string_funcs = engine.functions(Some("String"));
91//! assert!(string_funcs.iter().any(|f| f.name == "upper"));
92//!
93//! // Search by keyword (fuzzy matching, synonyms)
94//! let results = engine.search_functions("upper", 5);
95//! assert!(results.iter().any(|r| r.function.name == "upper"));
96//!
97//! // Get detailed function info
98//! let info = engine.describe_function("upper").unwrap();
99//! assert_eq!(info.category, "String");
100//!
101//! // Find similar functions
102//! let similar = engine.similar_functions("upper").unwrap();
103//! assert!(!similar.same_category.is_empty());
104//! ```
105//!
106//! ## JSON Utilities
107//!
108//! Beyond JMESPath evaluation, the engine provides JSON manipulation tools:
109//!
110//! ```rust
111//! use jpx_engine::JpxEngine;
112//!
113//! let engine = JpxEngine::new();
114//!
115//! // Pretty-print JSON
116//! let formatted = engine.format_json(r#"{"a":1}"#, 2).unwrap();
117//! assert!(formatted.contains('\n'));
118//!
119//! // Generate JSON Patch (RFC 6902)
120//! let patch = engine.diff(r#"{"a": 1}"#, r#"{"a": 2}"#).unwrap();
121//!
122//! // Apply JSON Patch
123//! let result = engine.patch(
124//!     r#"{"a": 1}"#,
125//!     r#"[{"op": "replace", "path": "/a", "value": 2}]"#
126//! ).unwrap();
127//!
128//! // Apply JSON Merge Patch (RFC 7396)
129//! let merged = engine.merge(
130//!     r#"{"a": 1, "b": 2}"#,
131//!     r#"{"b": 3, "c": 4}"#
132//! ).unwrap();
133//!
134//! // Analyze JSON structure
135//! let stats = engine.stats(r#"[1, 2, 3]"#).unwrap();
136//! assert_eq!(stats.root_type, "array");
137//! ```
138//!
139//! ## Query Store
140//!
141//! Store and reuse named queries within a session:
142//!
143//! ```rust
144//! use jpx_engine::JpxEngine;
145//! use serde_json::json;
146//!
147//! let engine = JpxEngine::new();
148//!
149//! // Define a reusable query
150//! engine.define_query(
151//!     "active_users".to_string(),
152//!     "users[?active].name".to_string(),
153//!     Some("Get names of active users".to_string())
154//! ).unwrap();
155//!
156//! // Run it by name
157//! let data = json!({"users": [
158//!     {"name": "alice", "active": true},
159//!     {"name": "bob", "active": false}
160//! ]});
161//! let result = engine.run_query("active_users", &data).unwrap();
162//! assert_eq!(result, json!(["alice"]));
163//!
164//! // List all stored queries
165//! let queries = engine.list_queries().unwrap();
166//! assert_eq!(queries.len(), 1);
167//! ```
168//!
169//! ## Configuration
170//!
171//! Load engine settings from `jpx.toml` files with layered discovery:
172//!
173//! ```rust
174//! use jpx_engine::{JpxEngine, EngineConfig};
175//!
176//! // Discover config from standard locations
177//! // (~/.config/jpx/jpx.toml, ./jpx.toml, $JPX_CONFIG)
178//! let config = EngineConfig::discover().unwrap();
179//! let engine = JpxEngine::from_config(config).unwrap();
180//!
181//! // Or use the builder for programmatic configuration
182//! let engine = JpxEngine::builder()
183//!     .strict(false)
184//!     .disable_category("geo")
185//!     .disable_function("env")
186//!     .build()
187//!     .unwrap();
188//! ```
189//!
190//! ## Tool Discovery
191//!
192//! Register and search tools across multiple servers (for MCP integration):
193//!
194//! ```rust
195//! use jpx_engine::{JpxEngine, DiscoverySpec};
196//! use serde_json::json;
197//!
198//! let engine = JpxEngine::new();
199//!
200//! // Register a server's tools
201//! let spec: DiscoverySpec = serde_json::from_value(json!({
202//!     "server": {"name": "my-server", "version": "1.0.0"},
203//!     "tools": [
204//!         {"name": "create_user", "description": "Create a new user", "tags": ["write"]}
205//!     ]
206//! })).unwrap();
207//!
208//! let result = engine.register_discovery(spec, false).unwrap();
209//! assert!(result.ok);
210//!
211//! // Search across registered tools
212//! let tools = engine.query_tools("user", 10).unwrap();
213//! assert!(!tools.is_empty());
214//! ```
215//!
216//! ## Strict Mode
217//!
218//! For standard JMESPath compliance without extensions:
219//!
220//! ```rust
221//! use jpx_engine::JpxEngine;
222//! use serde_json::json;
223//!
224//! let engine = JpxEngine::strict();
225//! assert!(engine.is_strict());
226//!
227//! // Standard functions work
228//! let result = engine.evaluate("length(@)", &json!([1, 2, 3])).unwrap();
229//! assert_eq!(result, json!(3));
230//!
231//! // Extension functions are not available for evaluation
232//! // (but introspection still works for documentation purposes)
233//! ```
234//!
235//! ## Architecture
236//!
237//! ```text
238//!    jpx-core           (parser, runtime, 400+ functions, registry)
239//!         |
240//!    jpx-engine         (this crate - evaluation, search, discovery, config)
241//!         |
242//!    +----+----+
243//!    |         |
244//!   jpx    jpx-mcp     (CLI and MCP transport)
245//! ```
246//!
247//! ## Thread Safety
248//!
249//! The engine uses interior mutability (`Arc<RwLock<...>>`) for the discovery
250//! registry and query store, making it safe to share across threads. The function
251//! registry is immutable after construction.
252
253mod bm25;
254pub mod config;
255mod discovery;
256mod error;
257mod eval;
258mod explain;
259mod introspection;
260mod json_utils;
261mod query_store;
262mod types;
263
264#[cfg(feature = "arrow")]
265pub mod arrow;
266
267pub use bm25::{Bm25Index, DocInfo, IndexOptions, SearchResult as Bm25SearchResult, TermInfo};
268pub use config::{EngineBuilder, EngineConfig, EngineSection, FunctionsSection, QueriesSection};
269pub use discovery::{
270    CategoryInfo, CategorySummary, DiscoveryRegistry, DiscoverySpec, ExampleSpec, IndexStats,
271    ParamSpec, RegistrationResult, ReturnSpec, ServerInfo, ServerSummary, ToolQueryResult,
272    ToolSpec,
273};
274pub use error::{EngineError, EvaluationErrorKind, Result};
275pub use explain::{ExplainResult, ExplainStep, has_let_nodes};
276pub use introspection::{FunctionDetail, SearchResult, SimilarFunctionsResult};
277pub use json_utils::{FieldAnalysis, PathInfo, StatsResult};
278pub use query_store::{QueryStore, StoredQuery};
279pub use types::{
280    BatchEvaluateResult, BatchExpressionResult, EvalRequest, EvalResponse, ValidationResult,
281};
282
283use discovery::DiscoveryRegistry as DiscoveryRegistryInner;
284use error::EngineError as EngineErrorInner;
285use query_store::QueryStore as QueryStoreInner;
286use serde_json::Value;
287use std::collections::HashMap;
288use std::sync::{Arc, RwLock};
289
290// Re-export commonly used types from jpx-core
291pub use jpx_core::ast;
292pub use jpx_core::query_library;
293pub use jpx_core::{Category, Expression, FunctionInfo, FunctionRegistry, Runtime, compile, parse};
294
295/// The JMESPath query engine.
296///
297/// `JpxEngine` is the main entry point for all jpx functionality. It combines:
298///
299/// - **JMESPath runtime** with 400+ extension functions
300/// - **Function registry** for introspection and search
301/// - **Discovery registry** for cross-server tool indexing
302/// - **Query store** for named query management
303///
304/// # Construction
305///
306/// ```rust
307/// use jpx_engine::JpxEngine;
308///
309/// // Full engine with all extensions
310/// let engine = JpxEngine::new();
311///
312/// // Strict mode (standard JMESPath only)
313/// let strict_engine = JpxEngine::strict();
314///
315/// // Or using Default
316/// let default_engine = JpxEngine::default();
317/// ```
318///
319/// # Thread Safety
320///
321/// The engine is designed to be shared across threads. The discovery registry
322/// and query store use `Arc<RwLock<...>>` for interior mutability, while the
323/// function registry is immutable after construction.
324///
325/// ```rust
326/// use jpx_engine::JpxEngine;
327/// use std::sync::Arc;
328///
329/// let engine = Arc::new(JpxEngine::new());
330///
331/// // Clone the Arc to share across threads
332/// let engine_clone = Arc::clone(&engine);
333/// std::thread::spawn(move || {
334///     let result = engine_clone.evaluate("length(@)", &serde_json::json!([1, 2, 3]));
335/// });
336/// ```
337pub struct JpxEngine {
338    /// JMESPath runtime with all extensions registered
339    pub(crate) runtime: Runtime,
340    /// Function registry for introspection
341    pub(crate) registry: FunctionRegistry,
342    /// Discovery registry for cross-server tool search
343    pub(crate) discovery: Arc<RwLock<DiscoveryRegistryInner>>,
344    /// Query store for named queries
345    pub(crate) queries: Arc<RwLock<QueryStoreInner>>,
346    /// Whether to use strict mode (standard JMESPath only)
347    pub(crate) strict: bool,
348}
349
350impl JpxEngine {
351    /// Creates a new engine with all extension functions enabled.
352    ///
353    /// This is the standard way to create an engine with full functionality,
354    /// including all 400+ extension functions.
355    ///
356    /// # Example
357    ///
358    /// ```rust
359    /// use jpx_engine::JpxEngine;
360    /// use serde_json::json;
361    ///
362    /// let engine = JpxEngine::new();
363    ///
364    /// // Standard JMESPath works
365    /// let result = engine.evaluate("name", &json!({"name": "alice"})).unwrap();
366    /// assert_eq!(result, json!("alice"));
367    ///
368    /// // Extension functions also work
369    /// let result = engine.evaluate("upper(name)", &json!({"name": "alice"})).unwrap();
370    /// assert_eq!(result, json!("ALICE"));
371    /// ```
372    pub fn new() -> Self {
373        Self::with_options(false)
374    }
375
376    /// Creates a new engine with configurable strict mode.
377    ///
378    /// # Arguments
379    ///
380    /// * `strict` - If `true`, only standard JMESPath functions are available
381    ///   for evaluation. Introspection features still show all functions.
382    ///
383    /// # Example
384    ///
385    /// ```rust
386    /// use jpx_engine::JpxEngine;
387    ///
388    /// // Create engine with extensions
389    /// let full_engine = JpxEngine::with_options(false);
390    ///
391    /// // Create strict engine (standard JMESPath only)
392    /// let strict_engine = JpxEngine::with_options(true);
393    /// assert!(strict_engine.is_strict());
394    /// ```
395    pub fn with_options(strict: bool) -> Self {
396        let mut runtime = Runtime::new();
397        runtime.register_builtin_functions();
398
399        let mut registry = FunctionRegistry::new();
400        registry.register_all();
401
402        if !strict {
403            registry.apply(&mut runtime);
404        }
405
406        Self {
407            runtime,
408            registry,
409            discovery: Arc::new(RwLock::new(DiscoveryRegistryInner::new())),
410            queries: Arc::new(RwLock::new(QueryStoreInner::new())),
411            strict,
412        }
413    }
414
415    /// Creates a new engine in strict mode (standard JMESPath only).
416    ///
417    /// Equivalent to `JpxEngine::with_options(true)`.
418    ///
419    /// # Example
420    ///
421    /// ```rust
422    /// use jpx_engine::JpxEngine;
423    /// use serde_json::json;
424    ///
425    /// let engine = JpxEngine::strict();
426    ///
427    /// // Standard functions work
428    /// let result = engine.evaluate("length(@)", &json!([1, 2, 3])).unwrap();
429    /// assert_eq!(result, json!(3));
430    /// ```
431    pub fn strict() -> Self {
432        Self::with_options(true)
433    }
434
435    /// Creates a new engine from an [`EngineConfig`].
436    ///
437    /// Applies function filtering, loads query libraries and inline queries,
438    /// and sets engine options from the config.
439    ///
440    /// # Example
441    ///
442    /// ```rust
443    /// use jpx_engine::{JpxEngine, EngineConfig};
444    ///
445    /// let config = EngineConfig::default();
446    /// let engine = JpxEngine::from_config(config).unwrap();
447    /// ```
448    pub fn from_config(config: EngineConfig) -> Result<Self> {
449        let strict = config.engine.strict;
450        let (runtime, registry) = config::build_runtime_from_config(&config.functions, strict);
451
452        let discovery = Arc::new(RwLock::new(DiscoveryRegistryInner::new()));
453        let queries = Arc::new(RwLock::new(QueryStoreInner::new()));
454
455        // Load queries from config
456        config::load_queries_into_store(&config.queries, &runtime, &queries)?;
457
458        Ok(Self {
459            runtime,
460            registry,
461            discovery,
462            queries,
463            strict,
464        })
465    }
466
467    /// Returns a builder for constructing a `JpxEngine` with programmatic overrides.
468    ///
469    /// # Example
470    ///
471    /// ```rust
472    /// use jpx_engine::JpxEngine;
473    ///
474    /// let engine = JpxEngine::builder()
475    ///     .strict(false)
476    ///     .disable_category("geo")
477    ///     .build()
478    ///     .unwrap();
479    /// ```
480    pub fn builder() -> config::EngineBuilder {
481        config::EngineBuilder::new()
482    }
483
484    /// Returns `true` if the engine is in strict mode.
485    ///
486    /// In strict mode, only standard JMESPath functions are available for
487    /// evaluation. Extension functions will cause evaluation errors.
488    pub fn is_strict(&self) -> bool {
489        self.strict
490    }
491
492    /// Returns a reference to the underlying JMESPath runtime.
493    ///
494    /// This provides access to the low-level runtime for advanced use cases.
495    pub fn runtime(&self) -> &Runtime {
496        &self.runtime
497    }
498
499    /// Returns a reference to the function registry.
500    ///
501    /// The registry contains metadata about all available functions and can
502    /// be used for custom introspection beyond what the engine methods provide.
503    pub fn registry(&self) -> &FunctionRegistry {
504        &self.registry
505    }
506
507    /// Returns a reference to the discovery registry.
508    ///
509    /// The discovery registry manages cross-server tool indexing and search.
510    /// Access is through `Arc<RwLock<...>>` for thread-safe mutation.
511    pub fn discovery(&self) -> &Arc<RwLock<DiscoveryRegistryInner>> {
512        &self.discovery
513    }
514
515    /// Returns a reference to the query store.
516    ///
517    /// The query store manages named queries for the session.
518    /// Access is through `Arc<RwLock<...>>` for thread-safe mutation.
519    pub fn queries(&self) -> &Arc<RwLock<QueryStoreInner>> {
520        &self.queries
521    }
522
523    // =========================================================================
524    // Query store methods
525    // =========================================================================
526
527    /// Define (store) a named query.
528    pub fn define_query(
529        &self,
530        name: String,
531        expression: String,
532        description: Option<String>,
533    ) -> Result<Option<StoredQuery>> {
534        // Validate expression first
535        let validation = self.validate(&expression);
536        if !validation.valid {
537            return Err(EngineErrorInner::InvalidExpression(
538                validation
539                    .error
540                    .unwrap_or_else(|| "Invalid expression".to_string()),
541            ));
542        }
543
544        let query = StoredQuery {
545            name,
546            expression,
547            description,
548        };
549
550        self.queries
551            .write()
552            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
553            .define(query)
554            .pipe(Ok)
555    }
556
557    /// Get a stored query by name.
558    pub fn get_query(&self, name: &str) -> Result<Option<StoredQuery>> {
559        Ok(self
560            .queries
561            .read()
562            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
563            .get(name)
564            .cloned())
565    }
566
567    /// Delete a stored query.
568    pub fn delete_query(&self, name: &str) -> Result<Option<StoredQuery>> {
569        Ok(self
570            .queries
571            .write()
572            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
573            .delete(name))
574    }
575
576    /// List all stored queries.
577    pub fn list_queries(&self) -> Result<Vec<StoredQuery>> {
578        Ok(self
579            .queries
580            .read()
581            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
582            .list()
583            .into_iter()
584            .cloned()
585            .collect())
586    }
587
588    /// Run a stored query.
589    pub fn run_query(&self, name: &str, input: &Value) -> Result<Value> {
590        let query = self
591            .get_query(name)?
592            .ok_or_else(|| EngineErrorInner::QueryNotFound(name.to_string()))?;
593
594        self.evaluate(&query.expression, input)
595    }
596
597    // =========================================================================
598    // Discovery methods
599    // =========================================================================
600
601    /// Register a discovery spec.
602    pub fn register_discovery(
603        &self,
604        spec: DiscoverySpec,
605        replace: bool,
606    ) -> Result<RegistrationResult> {
607        Ok(self
608            .discovery
609            .write()
610            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
611            .register(spec, replace))
612    }
613
614    /// Unregister a server from discovery.
615    pub fn unregister_discovery(&self, server_name: &str) -> Result<bool> {
616        Ok(self
617            .discovery
618            .write()
619            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
620            .unregister(server_name))
621    }
622
623    /// Query tools across registered servers.
624    pub fn query_tools(&self, query: &str, top_k: usize) -> Result<Vec<ToolQueryResult>> {
625        Ok(self
626            .discovery
627            .read()
628            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
629            .query(query, top_k))
630    }
631
632    /// Find tools similar to a given tool.
633    pub fn similar_tools(&self, tool_id: &str, top_k: usize) -> Result<Vec<ToolQueryResult>> {
634        Ok(self
635            .discovery
636            .read()
637            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
638            .similar(tool_id, top_k))
639    }
640
641    /// List all registered discovery servers.
642    pub fn list_discovery_servers(&self) -> Result<Vec<ServerSummary>> {
643        Ok(self
644            .discovery
645            .read()
646            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
647            .list_servers())
648    }
649
650    /// List discovery categories.
651    pub fn list_discovery_categories(&self) -> Result<HashMap<String, CategorySummary>> {
652        Ok(self
653            .discovery
654            .read()
655            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
656            .list_categories())
657    }
658
659    /// Get discovery index stats.
660    pub fn discovery_index_stats(&self) -> Result<Option<IndexStats>> {
661        Ok(self
662            .discovery
663            .read()
664            .map_err(|e| EngineErrorInner::Internal(e.to_string()))?
665            .index_stats())
666    }
667
668    /// Get the discovery schema.
669    pub fn get_discovery_schema(&self) -> Value {
670        DiscoveryRegistryInner::get_schema()
671    }
672}
673
674impl Default for JpxEngine {
675    fn default() -> Self {
676        Self::new()
677    }
678}
679
680/// Extension trait for pipe-style method chaining
681trait Pipe: Sized {
682    fn pipe<T, F: FnOnce(Self) -> T>(self, f: F) -> T {
683        f(self)
684    }
685}
686
687impl<T> Pipe for T {}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use serde_json::json;
693
694    #[test]
695    fn test_engine_creation() {
696        let engine = JpxEngine::new();
697        assert!(!engine.is_strict());
698    }
699
700    #[test]
701    fn test_engine_strict_mode() {
702        let engine = JpxEngine::strict();
703        assert!(engine.is_strict());
704    }
705
706    #[test]
707    fn test_engine_default() {
708        let engine = JpxEngine::default();
709        assert!(!engine.is_strict());
710    }
711
712    #[test]
713    fn test_query_store() {
714        let engine = JpxEngine::new();
715
716        // Define a query
717        engine
718            .define_query("count".to_string(), "length(@)".to_string(), None)
719            .unwrap();
720
721        // Get it
722        let query = engine.get_query("count").unwrap().unwrap();
723        assert_eq!(query.expression, "length(@)");
724
725        // Run it
726        let result = engine.run_query("count", &json!([1, 2, 3])).unwrap();
727        assert_eq!(result, json!(3));
728
729        // List queries
730        let queries = engine.list_queries().unwrap();
731        assert_eq!(queries.len(), 1);
732
733        // Delete it
734        engine.delete_query("count").unwrap();
735        assert!(engine.get_query("count").unwrap().is_none());
736    }
737
738    #[test]
739    fn test_discovery() {
740        let engine = JpxEngine::new();
741
742        let spec: DiscoverySpec = serde_json::from_value(json!({
743            "server": {"name": "test-server", "version": "1.0.0"},
744            "tools": [
745                {"name": "test_tool", "description": "A test tool", "tags": ["test"]}
746            ]
747        }))
748        .unwrap();
749
750        // Register
751        let result = engine.register_discovery(spec, false).unwrap();
752        assert!(result.ok);
753        assert_eq!(result.tools_indexed, 1);
754
755        // List servers
756        let servers = engine.list_discovery_servers().unwrap();
757        assert_eq!(servers.len(), 1);
758
759        // Query tools
760        let tools = engine.query_tools("test", 10).unwrap();
761        assert!(!tools.is_empty());
762
763        // Unregister
764        assert!(engine.unregister_discovery("test-server").unwrap());
765        assert!(engine.list_discovery_servers().unwrap().is_empty());
766    }
767
768    // =========================================================================
769    // Construction tests
770    // =========================================================================
771
772    #[test]
773    fn test_with_options_non_strict() {
774        let engine = JpxEngine::with_options(false);
775        assert!(!engine.is_strict());
776    }
777
778    #[test]
779    fn test_with_options_strict() {
780        let engine = JpxEngine::with_options(true);
781        assert!(engine.is_strict());
782    }
783
784    #[test]
785    fn test_from_config_default() {
786        let config = EngineConfig::default();
787        let engine = JpxEngine::from_config(config).unwrap();
788        assert!(!engine.is_strict());
789    }
790
791    #[test]
792    fn test_builder_default() {
793        let engine = JpxEngine::builder().build().unwrap();
794        assert!(!engine.is_strict());
795    }
796
797    // =========================================================================
798    // Accessor tests
799    // =========================================================================
800
801    #[test]
802    fn test_runtime_accessor() {
803        let engine = JpxEngine::new();
804        let runtime = engine.runtime();
805        // Verify we can compile an expression through the runtime reference
806        let expr = runtime.compile("length(@)").unwrap();
807        let data = json!([1, 2, 3]);
808        let result = expr.search(&data).unwrap();
809        assert_eq!(result, json!(3));
810    }
811
812    #[test]
813    fn test_registry_accessor() {
814        let engine = JpxEngine::new();
815        let registry = engine.registry();
816        // The registry should contain functions after register_all()
817        assert!(registry.get_function("upper").is_some());
818        assert!(registry.get_function("lower").is_some());
819        assert!(registry.is_enabled("upper"));
820    }
821
822    #[test]
823    fn test_discovery_accessor() {
824        let engine = JpxEngine::new();
825        let discovery = engine.discovery();
826        // Should be able to acquire a read lock and inspect the empty registry
827        let guard = discovery.read().unwrap();
828        assert!(guard.list_servers().is_empty());
829    }
830
831    #[test]
832    fn test_queries_accessor() {
833        let engine = JpxEngine::new();
834        let queries = engine.queries();
835        // Should be able to acquire a read lock and inspect the empty store
836        let guard = queries.read().unwrap();
837        assert!(guard.is_empty());
838    }
839
840    // =========================================================================
841    // Query store tests (via engine)
842    // =========================================================================
843
844    #[test]
845    fn test_define_query_with_description() {
846        let engine = JpxEngine::new();
847        engine
848            .define_query(
849                "named".to_string(),
850                "length(@)".to_string(),
851                Some("Counts elements".to_string()),
852            )
853            .unwrap();
854
855        let query = engine.get_query("named").unwrap().unwrap();
856        assert_eq!(query.expression, "length(@)");
857        assert_eq!(query.description, Some("Counts elements".to_string()));
858    }
859
860    #[test]
861    fn test_define_query_invalid_expression() {
862        let engine = JpxEngine::new();
863        let result = engine.define_query("bad".to_string(), "invalid[".to_string(), None);
864        assert!(result.is_err());
865        match result.unwrap_err() {
866            EngineError::InvalidExpression(_) => {} // expected
867            other => panic!("Expected InvalidExpression, got {:?}", other),
868        }
869    }
870
871    #[test]
872    fn test_define_query_overwrite() {
873        let engine = JpxEngine::new();
874
875        // First define returns None (no previous query)
876        let first = engine
877            .define_query("q".to_string(), "length(@)".to_string(), None)
878            .unwrap();
879        assert!(first.is_none());
880
881        // Second define with same name returns Some(old)
882        let second = engine
883            .define_query("q".to_string(), "keys(@)".to_string(), None)
884            .unwrap();
885        assert!(second.is_some());
886        let old = second.unwrap();
887        assert_eq!(old.expression, "length(@)");
888
889        // Current value should be the new expression
890        let current = engine.get_query("q").unwrap().unwrap();
891        assert_eq!(current.expression, "keys(@)");
892    }
893
894    #[test]
895    fn test_get_query_nonexistent() {
896        let engine = JpxEngine::new();
897        let result = engine.get_query("nonexistent").unwrap();
898        assert!(result.is_none());
899    }
900
901    #[test]
902    fn test_delete_query_nonexistent() {
903        let engine = JpxEngine::new();
904        let result = engine.delete_query("nonexistent").unwrap();
905        assert!(result.is_none());
906    }
907
908    #[test]
909    fn test_run_query_not_found() {
910        let engine = JpxEngine::new();
911        let result = engine.run_query("nonexistent", &json!({}));
912        assert!(result.is_err());
913        match result.unwrap_err() {
914            EngineError::QueryNotFound(name) => assert_eq!(name, "nonexistent"),
915            other => panic!("Expected QueryNotFound, got {:?}", other),
916        }
917    }
918
919    #[test]
920    fn test_list_queries_empty() {
921        let engine = JpxEngine::new();
922        let queries = engine.list_queries().unwrap();
923        assert!(queries.is_empty());
924    }
925
926    #[test]
927    fn test_list_queries_multiple() {
928        let engine = JpxEngine::new();
929        engine
930            .define_query("alpha".to_string(), "a".to_string(), None)
931            .unwrap();
932        engine
933            .define_query("beta".to_string(), "b".to_string(), None)
934            .unwrap();
935        engine
936            .define_query("gamma".to_string(), "c".to_string(), None)
937            .unwrap();
938
939        let queries = engine.list_queries().unwrap();
940        assert_eq!(queries.len(), 3);
941    }
942
943    // =========================================================================
944    // Discovery tests (via engine)
945    // =========================================================================
946
947    #[test]
948    fn test_register_discovery_duplicate() {
949        let engine = JpxEngine::new();
950
951        let spec: DiscoverySpec = serde_json::from_value(json!({
952            "server": {"name": "dup-server", "version": "1.0.0"},
953            "tools": [
954                {"name": "tool_a", "description": "Tool A", "tags": ["test"]}
955            ]
956        }))
957        .unwrap();
958
959        // First registration succeeds
960        let first = engine.register_discovery(spec.clone(), false).unwrap();
961        assert!(first.ok);
962
963        // Second registration without replace fails
964        let second = engine.register_discovery(spec, false).unwrap();
965        assert!(!second.ok);
966        assert!(second.warnings[0].contains("already registered"));
967    }
968
969    #[test]
970    fn test_unregister_nonexistent() {
971        let engine = JpxEngine::new();
972        let result = engine.unregister_discovery("nonexistent").unwrap();
973        assert!(!result);
974    }
975
976    #[test]
977    fn test_discovery_index_stats_empty() {
978        let engine = JpxEngine::new();
979        let stats = engine.discovery_index_stats().unwrap();
980        assert!(stats.is_none());
981    }
982
983    #[test]
984    fn test_get_discovery_schema() {
985        let engine = JpxEngine::new();
986        let schema = engine.get_discovery_schema();
987        assert!(schema.is_object());
988        assert!(schema.get("$schema").is_some());
989        assert_eq!(
990            schema.get("$schema").unwrap().as_str().unwrap(),
991            "http://json-schema.org/draft-07/schema#"
992        );
993        assert!(schema.get("title").is_some());
994        assert!(schema.get("properties").is_some());
995    }
996}