oxirouter 0.1.0

Autonomous Semantic Federation Engine for the Edge - Learned source selection for SPARQL federated queries with context-awareness
Documentation
//! Integration tests for the `void` feature: VoID/Turtle source-capability descriptors.

#[cfg(feature = "void")]
mod void_tests {
    use oxirouter::parse_oxirouter_ttl;
    use oxirouter::{DataSource, Router, SourceKind};

    // ─────────────────────────────────────────────────────────────────────────
    // Helpers
    // ─────────────────────────────────────────────────────────────────────────

    /// Minimal prefix preamble used across test fixtures.
    fn prefixes() -> &'static str {
        r#"@prefix void: <http://rdfs.org/ns/void#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix oxirouter: <http://oxirouter.rs/ns/> .
@prefix schema: <http://schema.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
"#
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 1: single dataset with endpoint, vocabularies, region
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_single_dataset() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    void:vocabulary <http://schema.org/> ;
    void:vocabulary <http://xmlns.com/foaf/0.1/> ;
    dcterms:spatial "EU" .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(
            sources.len(),
            1,
            "expected exactly 1 source, got {:?}",
            sources.len()
        );
        let src = &sources[0];
        assert_eq!(src.endpoint, "http://example.org/sparql");
        assert!(
            src.vocabularies.contains("http://schema.org/"),
            "missing schema vocabulary"
        );
        assert!(
            src.vocabularies.contains("http://xmlns.com/foaf/0.1/"),
            "missing foaf vocabulary"
        );
        assert!(src.regions.contains(&"EU".to_string()), "missing EU region");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 2: multiple datasets
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_multiple_datasets() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://ds1.example.org/sparql> .

<http://example.org/ds2> a void:Dataset ;
    void:sparqlEndpoint <http://ds2.example.org/sparql> .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 2, "expected 2 sources");
        let endpoints: std::collections::HashSet<&str> =
            sources.iter().map(|s| s.endpoint.as_str()).collect();
        assert!(endpoints.contains("http://ds1.example.org/sparql"));
        assert!(endpoints.contains("http://ds2.example.org/sparql"));
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 3: vocabulary prefix expansion
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_vocabulary_expansion() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    void:vocabulary schema:Thing .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        // schema:Thing should be expanded to http://schema.org/Thing
        assert!(
            sources[0].vocabularies.contains("http://schema.org/Thing"),
            "vocabulary not expanded; vocabularies = {:?}",
            sources[0].vocabularies
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 4: region mapping (strip quotes)
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_region_mapping() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    dcterms:spatial "EU" .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        assert!(
            sources[0].regions.contains(&"EU".to_string()),
            "expected region 'EU', got {:?}",
            sources[0].regions
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 5: P2pIpfs kind with multiaddr
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_kind_p2p_ipfs() {
        let ttl = format!(
            r#"{}
<http://example.org/ipfs-node> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    oxirouter:kind oxirouter:P2pIpfs ;
    oxirouter:multiaddr "/ip4/127.0.0.1/tcp/4001" .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        assert_eq!(
            sources[0].kind,
            SourceKind::P2pIpfs {
                multiaddr: "/ip4/127.0.0.1/tcp/4001".to_string()
            },
            "unexpected kind: {:?}",
            sources[0].kind
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 6: register_from_void_ttl on a Router
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_register_from_void_ttl() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> .
"#,
            prefixes()
        );
        let mut router = Router::new();
        router.register_from_void_ttl(&ttl).unwrap();
        assert_eq!(router.source_count(), 1, "expected 1 registered source");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 7: invalid TTL (unclosed IRI) — should return Err or empty sources
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_invalid_ttl_error() {
        // An unclosed IRI reference — the tokenizer produces a partial token;
        // the parser should either return Err or gracefully return zero sources.
        let ttl = r#"<http://unclosed.example.org/ds1 a <http://rdfs.org/ns/void#Dataset> ."#;
        let result = parse_oxirouter_ttl(ttl);
        // Either an error or an empty list is acceptable: the point is no panic.
        // Parser was lenient — assert nothing useful was extracted
        // (the unclosed IRI means the subject IRI was mangled or empty).
        if let Ok(sources) = result {
            let _ = sources; // anything is fine, no crash
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 8: blank node property list as subject
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_blank_node_property_list() {
        let ttl = format!(
            r#"{}
[ a void:Dataset ;
  void:sparqlEndpoint <http://bn.example.org/sparql> ] .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(
            sources.len(),
            1,
            "blank node dataset not found; sources = {:?}",
            sources
                .iter()
                .map(|s| s.endpoint.clone())
                .collect::<Vec<_>>()
        );
        assert_eq!(sources[0].endpoint, "http://bn.example.org/sparql");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 9 (bonus): semicolon continuation — multiple predicates on same subject
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_semicolon_continuation() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    void:vocabulary <http://schema.org/> ;
    dcterms:spatial "DE" .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        assert_eq!(sources[0].endpoint, "http://example.org/sparql");
        assert!(sources[0].vocabularies.contains("http://schema.org/"));
        assert!(sources[0].regions.contains(&"DE".to_string()));
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 10 (bonus): comma multi-object — multiple vocabularies via ','
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_comma_multi_object() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    void:vocabulary <http://schema.org/>, <http://xmlns.com/foaf/0.1/>, <http://dbpedia.org/ontology/> .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        assert!(sources[0].vocabularies.contains("http://schema.org/"));
        assert!(
            sources[0]
                .vocabularies
                .contains("http://xmlns.com/foaf/0.1/")
        );
        assert!(
            sources[0]
                .vocabularies
                .contains("http://dbpedia.org/ontology/")
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 11: priority parsing from string literal
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_priority() {
        let ttl = format!(
            r#"{}
<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    oxirouter:priority "0.75" .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        // Allow small floating-point tolerance
        assert!(
            (sources[0].priority - 0.75_f32).abs() < 1e-5,
            "expected priority 0.75, got {}",
            sources[0].priority
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 12: P2pLibp2p kind with peer_id
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_parse_kind_p2p_libp2p() {
        let ttl = format!(
            r#"{}
<http://example.org/libp2p-node> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    oxirouter:kind oxirouter:P2pLibp2p ;
    oxirouter:peerId "QmTestPeerIdAbcdef" .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(sources.len(), 1);
        assert_eq!(
            sources[0].kind,
            SourceKind::P2pLibp2p {
                peer_id: "QmTestPeerIdAbcdef".to_string()
            },
            "unexpected kind: {:?}",
            sources[0].kind
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 13: dataset without endpoint is skipped
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_dataset_without_endpoint_skipped() {
        let ttl = format!(
            r#"{}
<http://example.org/ds-no-ep> a void:Dataset ;
    void:vocabulary <http://schema.org/> .
"#,
            prefixes()
        );
        let sources = parse_oxirouter_ttl(&ttl).unwrap();
        assert_eq!(
            sources.len(),
            0,
            "dataset without endpoint should be skipped"
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 14: SPARQL-style PREFIX directive (uppercase)
    // ─────────────────────────────────────────────────────────────────────────
    #[test]
    fn test_sparql_style_prefix_directive() {
        let ttl = r#"PREFIX void: <http://rdfs.org/ns/void#>
PREFIX dcterms: <http://purl.org/dc/terms/>

<http://example.org/ds1> a void:Dataset ;
    void:sparqlEndpoint <http://example.org/sparql> ;
    dcterms:spatial "JP" .
"#;
        let sources = parse_oxirouter_ttl(ttl).unwrap();
        assert_eq!(sources.len(), 1);
        assert!(sources[0].regions.contains(&"JP".to_string()));
    }

    // Ensure DataSource is importable directly (suppresses "unused import" on the use above)
    #[test]
    fn test_datasource_import_smoke() {
        let _ds: DataSource = DataSource::new("smoke", "http://example.org/sparql");
    }
}