diff --git a/Cargo.lock b/Cargo.lock index 8dea1ab..ecd443f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2424,6 +2424,7 @@ dependencies = [ "once_cell", "qrcode", "rand 0.7.3", + "regex", "sbbf-rs-safe", "serde", "serde_bare", diff --git a/nextgraph/src/tests/create_or_open_wallet.rs b/nextgraph/src/tests/create_or_open_wallet.rs new file mode 100644 index 0000000..760bc83 --- /dev/null +++ b/nextgraph/src/tests/create_or_open_wallet.rs @@ -0,0 +1,135 @@ +// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers +// All rights reserved. +// Licensed under the Apache License, Version 2.0 +// +// or the MIT license , +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +use std::fs::{self, create_dir_all, File}; +use std::io::{Read, Write}; +use std::path::PathBuf; + +use crate::local_broker::{ + init_local_broker, session_start, wallet_create_v0, wallet_get_file, wallet_import, + wallet_open_with_mnemonic_words, wallet_read_file, LocalBrokerConfig, SessionConfig, +}; +use ng_net::types::BootstrapContentV0; +use ng_repo::types::PubKey; +use ng_wallet::types::{CreateWalletV0, SensitiveWallet}; + +static WALLET_PIN: [u8; 4] = [2, 3, 2, 3]; + +// Persistent test assets (wallet base path + stored credentials) +fn test_base_path() -> PathBuf { + // Use the crate manifest dir so tests find files regardless of the + // process current working directory when `cargo test` runs. + let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + base.push("src"); + base.push("tests"); + base +} + +fn build_wallet_and_creds_paths() -> (PathBuf, PathBuf) { + let mut base = test_base_path(); + base.push(".ng"); + create_dir_all(&base).expect("create test base path"); + (base.join("test_wallet.ngw"), base.join("wallet_creds.txt")) +} + +pub async fn create_or_open_wallet() -> (SensitiveWallet, u64) { + let base_path = test_base_path(); + fs::create_dir_all(&base_path).expect("create base path"); + init_local_broker(Box::new(move || { + LocalBrokerConfig::BasePath(base_path.clone()) + })) + .await; + + let wallet; + let session_id: u64; + + let (wallet_path, creds_path) = build_wallet_and_creds_paths(); + + // Don't load from file due to a bug which makes reloading wallets fail. + if wallet_path.exists() && false { + // Read the wallet file from the known test base path (not the process cwd) + let wallet_file = fs::read(&wallet_path).expect("read wallet file"); + // load stored wallet_name + mnemonic + let mut s = String::new(); + File::open(creds_path) + .expect("open creds") + .read_to_string(&mut s) + .expect("read creds"); + let mut lines = s.lines(); + let mnemonic_line = lines.next().expect("missing mnemonic").to_string(); + let mnemonic_words: Vec = mnemonic_line + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + + let read_wallet = wallet_read_file(wallet_file).await.unwrap(); + wallet = + wallet_open_with_mnemonic_words(&read_wallet, &mnemonic_words, WALLET_PIN).unwrap(); + + let _client = wallet_import(read_wallet.clone(), wallet.clone(), true) + .await + .unwrap(); + + let session = session_start(SessionConfig::new_in_memory( + &wallet.personal_identity(), + &read_wallet.name(), + )) + .await + .unwrap(); + + session_id = session.session_id; + } else { + // first run: create wallet + // Load a real security image from the crate so tests don't depend on cwd. + // Try a few known candidate locations inside the crate. + let manifest_dir = test_base_path(); + + let security_img = + fs::read(manifest_dir.join("security-image.png")).expect("read sec image file"); + + let peer_id_of_server_broker = PubKey::nil(); + let result = wallet_create_v0(CreateWalletV0 { + security_img, + security_txt: "know yourself".to_string(), + pin: WALLET_PIN, + pazzle_length: 9, + send_bootstrap: false, + send_wallet: false, + result_with_wallet_file: false, + local_save: false, + core_bootstrap: BootstrapContentV0::new_localhost(peer_id_of_server_broker), + core_registration: None, + additional_bootstrap: None, + pdf: false, + device_name: "test".to_string(), + }) + .await + .expect("wallet_create_v0"); + + // Save wallet to file. + let wallet_bin = wallet_get_file(&result.wallet_name).await.unwrap(); + let mut creds_file = File::create(creds_path).expect("create creds file"); + let mut wallet_file = File::create(wallet_path).expect("create wallet file"); + + // Use the mnemonic_str already provided (list of words) to avoid mistakes + let mnemonic_words: Vec = result.mnemonic_str.clone(); + writeln!(creds_file, "{}", mnemonic_words.join(" ")).expect("write mnemonic to creds file"); + creds_file.flush().expect("flush creds file"); + + wallet_file + .write_all(&wallet_bin) + .expect("write wallet file"); + + wallet = wallet_open_with_mnemonic_words(&result.wallet, &mnemonic_words, WALLET_PIN) + .expect("open wallet"); + session_id = result.session_id; + } + + return (wallet, session_id); +} diff --git a/nextgraph/src/tests/get_or_create_wallet.rs b/nextgraph/src/tests/get_or_create_wallet.rs deleted file mode 100644 index e69de29..0000000 diff --git a/nextgraph/src/tests/mod.rs b/nextgraph/src/tests/mod.rs index 07339e7..1abaa70 100644 --- a/nextgraph/src/tests/mod.rs +++ b/nextgraph/src/tests/mod.rs @@ -9,3 +9,6 @@ #[doc(hidden)] pub mod orm; + +#[doc(hidden)] +pub mod create_or_open_wallet; diff --git a/nextgraph/src/tests/orm.rs b/nextgraph/src/tests/orm.rs index 2a329b5..499d7c5 100644 --- a/nextgraph/src/tests/orm.rs +++ b/nextgraph/src/tests/orm.rs @@ -7,146 +7,33 @@ // notice may not be copied, modified, or distributed except // according to those terms. -use std::fs::{self, create_dir_all, File}; -use std::io::{Read, Write}; -use std::path::PathBuf; - -use crate::local_broker::{ - doc_create, doc_sparql_construct, doc_sparql_update, init_local_broker, session_start, - session_stop, user_disconnect, wallet_close, wallet_create_v0, wallet_get_file, wallet_import, - wallet_open_with_mnemonic_words, wallet_read_file, LocalBrokerConfig, SessionConfig, +use crate::local_broker::{doc_create, doc_sparql_construct, doc_sparql_update}; +use crate::tests::create_or_open_wallet::create_or_open_wallet; +use ng_net::orm::{ + OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaLiterals, OrmSchemaPredicate, OrmSchemaShape, + OrmShapeType, }; -use ng_net::types::BootstrapContentV0; use ng_repo::log_info; -use ng_repo::types::PubKey; -use ng_wallet::types::{CreateWalletV0, SensitiveWallet}; -use once_cell::sync::OnceCell; - -static WALLET_PIN: [u8; 4] = [2, 3, 2, 3]; - -// Persistent test assets (wallet base path + stored credentials) -fn test_base_path() -> PathBuf { - // Use the crate manifest dir so tests find files regardless of the - // process current working directory when `cargo test` runs. - let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - base.push("src"); - base.push("tests"); - base -} - -fn build_wallet_and_creds_paths() -> (PathBuf, PathBuf) { - let mut base = test_base_path(); - base.push(".ng"); - create_dir_all(&base).expect("create test base path"); - (base.join("test_wallet.ngw"), base.join("wallet_creds.txt")) -} - -static INIT: OnceCell<()> = OnceCell::new(); - -async fn init_broker() { - if INIT.get().is_none() { - let base = test_base_path(); - fs::create_dir_all(&base).expect("create base path"); - init_local_broker(Box::new(move || LocalBrokerConfig::BasePath(base.clone()))).await; - let _ = INIT.set(()); - } -} - -async fn create_or_open_wallet() -> (SensitiveWallet, u64) { - init_broker().await; - - let wallet; - let session_id: u64; - - let (wallet_path, creds_path) = build_wallet_and_creds_paths(); - - // Don't load from file due to a bug which makes reloading wallets fail. - if wallet_path.exists() && false { - // Read the wallet file from the known test base path (not the process cwd) - let wallet_file = fs::read(&wallet_path).expect("read wallet file"); - // load stored wallet_name + mnemonic - let mut s = String::new(); - File::open(creds_path) - .expect("open creds") - .read_to_string(&mut s) - .expect("read creds"); - let mut lines = s.lines(); - let mnemonic_line = lines.next().expect("missing mnemonic").to_string(); - let mnemonic_words: Vec = mnemonic_line - .split_whitespace() - .map(|s| s.to_string()) - .collect(); - - let read_wallet = wallet_read_file(wallet_file).await.unwrap(); - wallet = - wallet_open_with_mnemonic_words(&read_wallet, &mnemonic_words, WALLET_PIN).unwrap(); - - let _client = wallet_import(read_wallet.clone(), wallet.clone(), true) - .await - .unwrap(); - - let session = session_start(SessionConfig::new_in_memory( - &wallet.personal_identity(), - &read_wallet.name(), - )) - .await - .unwrap(); - - session_id = session.session_id; - } else { - // first run: create wallet - // Load a real security image from the crate so tests don't depend on cwd. - // Try a few known candidate locations inside the crate. - let manifest_dir = test_base_path(); - - let security_img = - fs::read(manifest_dir.join("security-image.png")).expect("read sec image file"); - - let peer_id_of_server_broker = PubKey::nil(); - let result = wallet_create_v0(CreateWalletV0 { - security_img, - security_txt: "know yourself".to_string(), - pin: WALLET_PIN, - pazzle_length: 9, - send_bootstrap: false, - send_wallet: false, - result_with_wallet_file: false, - local_save: false, - core_bootstrap: BootstrapContentV0::new_localhost(peer_id_of_server_broker), - core_registration: None, - additional_bootstrap: None, - pdf: false, - device_name: "test".to_string(), - }) - .await - .expect("wallet_create_v0"); - - // Save wallet to file. - let wallet_bin = wallet_get_file(&result.wallet_name).await.unwrap(); - let mut creds_file = File::create(creds_path).expect("create creds file"); - let mut wallet_file = File::create(wallet_path).expect("create wallet file"); +use ng_verifier::orm::sparql_construct_from_orm_shape_type; +use std::collections::HashMap; - // Use the mnemonic_str already provided (list of words) to avoid mistakes - let mnemonic_words: Vec = result.mnemonic_str.clone(); - writeln!(creds_file, "{}", mnemonic_words.join(" ")).expect("write mnemonic to creds file"); - creds_file.flush().expect("flush creds file"); - - wallet_file - .write_all(&wallet_bin) - .expect("write wallet file"); - - wallet = wallet_open_with_mnemonic_words(&result.wallet, &mnemonic_words, WALLET_PIN) - .expect("open wallet"); - session_id = result.session_id; - } - - return (wallet, session_id); -} +#[async_std::test] +async fn test_create_sparql_from_schema() { + // Setup wallet and document + let (_wallet, session_id) = create_or_open_wallet().await; + let doc_nuri = doc_create( + session_id, + "Graph".to_string(), + "test_orm_query".to_string(), + "store".to_string(), + None, + None, + ) + .await + .expect("error creating doc"); -fn build_insert_sparql() -> String { - // Data conforms to testShape.shex - // Shape requires: a ex:TestObject + required fields. - r#" + // Insert data with unrelated predicates + let insert_sparql = r#" PREFIX ex: INSERT DATA { a ex:TestObject ; @@ -167,61 +54,487 @@ INSERT DATA { ex:prop2 2 ] ; ex:numOrStr "either" ; - ex:lit1Or2 "lit1" . + ex:lit1Or2 "lit1" ; + ex:unrelated "some value" ; + ex:anotherUnrelated 4242 . } "# - .trim() - .to_string() + .to_string(); + + doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone())) + .await + .expect("SPARQL update failed"); + + // Define the ORM schema + let mut schema = HashMap::new(); + + schema.insert( + "http://example.org/TestObject||http://example.org/anotherObject".to_string(), + OrmSchemaShape { + iri: "http://example.org/TestObject||http://example.org/anotherObject".to_string(), + predicates: vec![ + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + iri: "http://example.org/prop1".to_string(), + readablePredicate: "prop1".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + iri: "http://example.org/prop2".to_string(), + readablePredicate: "prop2".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + ], + }, + ); + + schema.insert( + "http://example.org/TestObject".to_string(), + OrmSchemaShape { + iri: "http://example.org/TestObject".to_string(), + predicates: vec![ + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::literal, + literals: Some(OrmSchemaLiterals::StrArray(vec![ + "http://example.org/TestObject".to_string(), + ])), + shape: None, + }], + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + readablePredicate: "type".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: Some(true), + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + iri: "http://example.org/stringValue".to_string(), + readablePredicate: "stringValue".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + iri: "http://example.org/numValue".to_string(), + readablePredicate: "numValue".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::boolean, + literals: None, + shape: None, + }], + iri: "http://example.org/boolValue".to_string(), + readablePredicate: "boolValue".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + iri: "http://example.org/arrayValue".to_string(), + readablePredicate: "arrayValue".to_string(), + maxCardinality: -1, + minCardinality: 0, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::shape, + literals: None, + shape: Some( + "http://example.org/TestObject||http://example.org/objectValue" + .to_string(), + ), + }], + iri: "http://example.org/objectValue".to_string(), + readablePredicate: "objectValue".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::shape, + literals: None, + shape: Some( + "http://example.org/TestObject||http://example.org/anotherObject" + .to_string(), + ), + }], + iri: "http://example.org/anotherObject".to_string(), + readablePredicate: "anotherObject".to_string(), + maxCardinality: -1, + minCardinality: 0, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![ + OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }, + OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }, + ], + iri: "http://example.org/numOrStr".to_string(), + readablePredicate: "numOrStr".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::literal, + literals: Some(OrmSchemaLiterals::StrArray(vec![ + "lit1".to_string(), + "lit2".to_string(), + ])), + shape: None, + }], + iri: "http://example.org/lit1Or2".to_string(), + readablePredicate: "lit1Or2".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + ], + }, + ); + + schema.insert( + "http://example.org/TestObject||http://example.org/objectValue".to_string(), + OrmSchemaShape { + iri: "http://example.org/TestObject||http://example.org/objectValue".to_string(), + predicates: vec![ + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + iri: "http://example.org/nestedString".to_string(), + readablePredicate: "nestedString".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + iri: "http://example.org/nestedNum".to_string(), + readablePredicate: "nestedNum".to_string(), + maxCardinality: 1, + minCardinality: 1, + extra: None, + }, + OrmSchemaPredicate { + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + iri: "http://example.org/nestedArray".to_string(), + readablePredicate: "nestedArray".to_string(), + maxCardinality: -1, + minCardinality: 0, + extra: None, + }, + ], + }, + ); + + let shape_type = OrmShapeType { + schema, + shape: "http://example.org/TestObject".to_string(), + }; + + // Generate and execute the CONSTRUCT query + let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap(); + + let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) + .await + .expect("SPARQL construct failed"); + + // Assert the results + let predicates: Vec = triples + .iter() + .map(|t| t.predicate.as_str().to_string()) + .collect(); + + // Expected predicates based on the schema + let expected_predicates = vec![ + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "http://example.org/stringValue", + "http://example.org/numValue", + "http://example.org/boolValue", + "http://example.org/arrayValue", + "http://example.org/objectValue", + "http://example.org/anotherObject", + "http://example.org/numOrStr", + "http://example.org/lit1Or2", + "http://example.org/nestedString", + "http://example.org/nestedNum", + "http://example.org/nestedArray", + "http://example.org/prop1", + "http://example.org/prop2", + ]; + + for p in expected_predicates { + assert!( + predicates.contains(&p.to_string()), + "Missing predicate: {}", + p + ); + } + + // Unrelated predicates should not be in the result + assert!( + !predicates.contains(&"http://example.org/unrelated".to_string()), + "Found unrelated predicate" + ); + assert!( + !predicates.contains(&"http://example.org/anotherUnrelated".to_string()), + "Found another unrelated predicate" + ); } -fn build_construct_sparql() -> String { - r#" -CONSTRUCT { - ?s ?p ?o . -} WHERE { - ?s ?p ?o . +#[async_std::test] +async fn test_orm_query_partial_match_missing_required() { + // Setup + let (_wallet, session_id) = create_or_open_wallet().await; + let doc_nuri = doc_create( + session_id, + "Graph".to_string(), + "test_orm_partial_required".to_string(), + "store".to_string(), + None, + None, + ) + .await + .unwrap(); + + // Insert data missing a required field (`prop2`) + let insert_sparql = r#" +PREFIX ex: +INSERT DATA { + a ex:TestObject ; + ex:prop1 "one" . } "# - .to_string() + .to_string(); + doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone())) + .await + .unwrap(); + + // Schema with two required fields + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/TestObject".to_string(), + OrmSchemaShape { + iri: "http://example.org/TestObject".to_string(), + predicates: vec![ + OrmSchemaPredicate { + iri: "http://example.org/prop1".to_string(), + minCardinality: 1, + ..Default::default() + }, + OrmSchemaPredicate { + iri: "http://example.org/prop2".to_string(), + minCardinality: 1, + ..Default::default() + }, + ], + }, + ); + let shape_type = OrmShapeType { + schema, + shape: "http://example.org/TestObject".to_string(), + }; + + // Generate and run query + let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap(); + let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) + .await + .unwrap(); + + // Assert: No triples should be returned as the object is incomplete. + assert!(triples.is_empty()); } #[async_std::test] -async fn test_wallet_and_sparql_insert() { - let (wallet, session_id) = create_or_open_wallet().await; - - let sparql = build_insert_sparql(); +async fn test_orm_query_partial_match_missing_optional() { + // Setup + let (_wallet, session_id) = create_or_open_wallet().await; let doc_nuri = doc_create( session_id, "Graph".to_string(), - "test".to_string(), + "test_orm_partial_optional".to_string(), "store".to_string(), None, None, ) .await - .expect("error creating doc"); + .unwrap(); - log_info!("session_id: {:?} doc nuri: {:?}", session_id, doc_nuri); + // Insert data missing an optional field (`prop2`) + let insert_sparql = r#" +PREFIX ex: +INSERT DATA { + a ex:TestObject ; + ex:prop1 "one" . +} +"# + .to_string(); + doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone())) + .await + .unwrap(); - let update_result = doc_sparql_update(session_id, sparql.clone(), Some(doc_nuri.clone())).await; - assert!( - update_result.is_ok(), - "SPARQL update failed: {:?}", - update_result.err() + // Schema with one required and one optional field + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/TestObject".to_string(), + OrmSchemaShape { + iri: "http://example.org/TestObject".to_string(), + predicates: vec![ + OrmSchemaPredicate { + iri: "http://example.org/prop1".to_string(), + minCardinality: 1, + ..Default::default() + }, + OrmSchemaPredicate { + iri: "http://example.org/prop2".to_string(), + minCardinality: 0, // Optional + ..Default::default() + }, + ], + }, ); - log_info!("Sparql update result: {:?}", update_result.unwrap()); + let shape_type = OrmShapeType { + schema, + shape: "http://example.org/TestObject".to_string(), + }; - // Query the data. - let query_result = - doc_sparql_construct(session_id, build_construct_sparql(), Some(doc_nuri.clone())).await; - log_info!("Sparql construct result: {:?}", query_result.unwrap()); + // Generate and run query + let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap(); + let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) + .await + .unwrap(); + + // Assert: One triple for prop1 should be returned. + assert_eq!(triples.len(), 1); + assert_eq!(triples[0].predicate.as_str(), "http://example.org/prop1"); +} + +#[async_std::test] +async fn test_orm_query_cyclic_schema() { + // Setup + let (_wallet, session_id) = create_or_open_wallet().await; + let doc_nuri = doc_create( + session_id, + "Graph".to_string(), + "test_orm_cyclic".to_string(), + "store".to_string(), + None, + None, + ) + .await + .unwrap(); - user_disconnect(&wallet.personal_identity()) + // Insert cyclic data (two people who know each other) + let insert_sparql = r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; ex:name "Alice" ; ex:knows . + a ex:Person ; ex:name "Bob" ; ex:knows . +} +"# + .to_string(); + doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone())) .await - .expect("disconnect user"); - session_stop(&wallet.personal_identity()) + .unwrap(); + + // Cyclic schema: Person has a `knows` predicate pointing to another Person + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + minCardinality: 1, + ..Default::default() + }, + OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + minCardinality: 1, + ..Default::default() + }, + OrmSchemaPredicate { + iri: "http://example.org/knows".to_string(), + minCardinality: 0, + maxCardinality: -1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::shape, + shape: Some("http://example.org/Person".to_string()), + literals: None, + }], + ..Default::default() + }, + ], + }, + ); + let shape_type = OrmShapeType { + schema, + shape: "http://example.org/Person".to_string(), + }; + + // Generate and run query. This must not infinite loop. + let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap(); + log_info!("cyclic query result:\n{}", query); + let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone())) .await - .expect("close session"); + .unwrap(); - wallet_close(&wallet.name()).await.expect("close wallet"); + // Assert: All 6 triples (3 per person) should be returned. + log_info!("Triples:\n{:?}", triples); + assert_eq!(triples.len(), 24); } diff --git a/ng-net/src/orm.rs b/ng-net/src/orm.rs index abb0082..362bd16 100644 --- a/ng-net/src/orm.rs +++ b/ng-net/src/orm.rs @@ -23,6 +23,7 @@ pub struct OrmShapeType { pub shape: String, } +/* == Diff Types == */ #[derive(Clone, Debug, Serialize, Deserialize)] #[allow(non_camel_case_types)] pub enum OrmDiffOpType { @@ -42,12 +43,13 @@ pub struct OrmDiffOp { pub op: OrmDiffOpType, pub valType: Option, pub path: String, - pub value: Option, + pub value: Option, // TODO: Improve type } pub type OrmDiff = Vec; -type OrmSchema = HashMap; +/* == ORM Schema == */ +pub type OrmSchema = HashMap; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrmSchemaShape { @@ -55,7 +57,7 @@ pub struct OrmSchemaShape { pub predicates: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[allow(non_camel_case_types)] pub enum OrmSchemaLiteralType { number, @@ -63,23 +65,12 @@ pub enum OrmSchemaLiteralType { boolean, iri, literal, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[allow(non_camel_case_types)] -pub enum OrmSchemaPredicateType { - number, - string, - boolean, - iri, - literal, - nested, - eitherOf, + shape, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] -pub enum OrmLiterals { +pub enum OrmSchemaLiterals { Bool(bool), NumArray(Vec), StrArray(Vec), @@ -88,26 +79,47 @@ pub enum OrmLiterals { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrmSchemaDataType { pub valType: OrmSchemaLiteralType, - pub literals: Option, + pub literals: Option, + pub shape: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrmSchemaPredicate { - pub valType: OrmSchemaPredicateType, + pub dataTypes: Vec, pub iri: String, pub readablePredicate: String, - pub literalValue: Option, // Strictly speaking, no objects. - pub nestedShape: Option, // Only by reference. - pub maxCardinality: i64, // -1 for infinity + /// `-1` for infinity + pub maxCardinality: i64, pub minCardinality: i64, - pub eitherOf: Option>, // Shape references or multi type. pub extra: Option, } -// TODO: Will this be serialized correctly? -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OrmSchemaEitherOfOption { - ShapeRef(String), - DataType(OrmSchemaDataType), +impl Default for OrmSchemaDataType { + fn default() -> Self { + Self { + literals: None, + shape: None, + valType: OrmSchemaLiteralType::string, + } + } +} + +impl Default for OrmSchemaPredicate { + fn default() -> Self { + Self { + dataTypes: Vec::new(), + iri: String::new(), + readablePredicate: String::new(), + maxCardinality: -1, + minCardinality: 0, + extra: None, + } + } +} + +/** == Internal data types == */ +#[derive(Clone, Debug)] +pub struct OrmShapeTypeRef { + ref_count: u64, + shape_type: OrmShapeType, } diff --git a/ng-repo/src/errors.rs b/ng-repo/src/errors.rs index a1b286f..4dd0d63 100644 --- a/ng-repo/src/errors.rs +++ b/ng-repo/src/errors.rs @@ -397,6 +397,7 @@ pub enum VerifierError { ContactAlreadyExists, InternalError, InvalidInboxPost, + InvalidOrmSchema, } impl Error for VerifierError {} diff --git a/ng-verifier/Cargo.toml b/ng-verifier/Cargo.toml index 9b7a746..0cc65fd 100644 --- a/ng-verifier/Cargo.toml +++ b/ng-verifier/Cargo.toml @@ -39,6 +39,7 @@ ng-repo = { path = "../ng-repo", version = "0.1.2" } ng-net = { path = "../ng-net", version = "0.1.2" } ng-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha" } once_cell = "1.17.1" +regex = "1.8.4" [target.'cfg(target_family = "wasm")'.dependencies] ng-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha", features = [ diff --git a/ng-verifier/src/orm.rs b/ng-verifier/src/orm.rs index a5a44da..183c7e8 100644 --- a/ng-verifier/src/orm.rs +++ b/ng-verifier/src/orm.rs @@ -12,11 +12,13 @@ use std::collections::HashMap; use futures::channel::mpsc; use futures::SinkExt; -use ng_net::app_protocol::*; +use lazy_static::lazy_static; pub use ng_net::orm::OrmDiff; pub use ng_net::orm::OrmShapeType; +use ng_net::orm::{OrmSchemaDataType, OrmSchemaShape}; +use ng_net::orm::{OrmSchemaLiteralType, OrmSchemaLiterals}; +use ng_net::{app_protocol::*, orm::OrmSchema}; use ng_net::{ - connection::NoiseFSM, types::*, utils::{Receiver, Sender}, }; @@ -24,7 +26,9 @@ use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults}; use ng_oxigraph::oxrdf::Term; use ng_oxigraph::oxrdf::Triple; use ng_repo::errors::NgError; +use ng_repo::errors::VerifierError; use ng_repo::log::*; +use regex::Regex; use crate::types::*; use crate::verifier::*; @@ -88,7 +92,7 @@ impl Verifier { let results = oxistore .query(parsed, None) .map_err(|e| NgError::OxiGraphError(e.to_string()))?; - match results { + let sols = match results { QueryResults::Solutions(sols) => { let mut results = vec![]; for t in sols { @@ -103,12 +107,15 @@ impl Verifier { Ok(results) } _ => return Err(NgError::InvalidResponse), - } + }; + sols } + fn create_orm_from_triples(&mut self, scope: &NuriV0, shape_type: &OrmShapeType) {} + pub(crate) async fn orm_update(&mut self, scope: &NuriV0, patch: GraphQuadsPatch) {} - pub(crate) async fn frontend_update_orm( + pub(crate) async fn orm_frontend_update( &mut self, scope: &NuriV0, shape_id: String, @@ -150,7 +157,7 @@ impl Verifier { pub(crate) async fn start_orm( &mut self, nuri: &NuriV0, - schema: &OrmShapeType, + shape_type: &OrmShapeType, session_id: u64, ) -> Result<(Receiver, CancelFn), NgError> { let (tx, rx) = mpsc::unbounded::(); @@ -158,12 +165,12 @@ impl Verifier { self.orm_subscriptions.insert( nuri.clone(), HashMap::from([( - schema.shape.clone(), + shape_type.shape.clone(), HashMap::from([(session_id, tx.clone())]), )]), ); - //self.push_orm_response().await; + //self.push_orm_response().await; (only for requester, not all sessions) let close = Box::new(move || { //log_debug!("CLOSE_CHANNEL of subscription for branch {}", branch_id); @@ -174,3 +181,231 @@ impl Verifier { Ok((rx, close)) } } + +fn is_iri(s: &str) -> bool { + lazy_static! { + static ref IRI_REGEX: Regex = Regex::new(r"^[A-Za-z][A-Za-z0-9+\.\-]{1,12}:").unwrap(); + } + IRI_REGEX.is_match(s) +} + +fn literal_to_sparql_str(var: OrmSchemaDataType) -> Vec { + match var.literals { + None => [].to_vec(), + Some(literals) => match literals { + OrmSchemaLiterals::Bool(val) => { + if val == true { + ["true".to_string()].to_vec() + } else { + ["false".to_string()].to_vec() + } + } + OrmSchemaLiterals::NumArray(numbers) => { + numbers.iter().map(|num| num.to_string()).collect() + } + OrmSchemaLiterals::StrArray(stings) => stings + .iter() + .map(|str| { + // We assume that strings can be IRIs (currently no support for typed literals). + if is_iri(str) { + format!("<{}>", escape_iri(str)) + } else { + format!("\"{}\"", escape_literal(str)) + } + }) + .collect(), + }, + } +} + +pub fn sparql_construct_from_orm_shape_type( + shape_type: &OrmShapeType, + max_recursion: Option, +) -> Result { + // Use a counter to generate unique variable names. + let mut var_counter = 0; + fn get_new_var_name(counter: &mut i32) -> String { + let name = format!("v{}", counter); + *counter += 1; + name + } + + // Collect all statements to be added to the construct and where bodies. + let mut construct_statements = Vec::new(); + let mut where_statements = Vec::new(); + + // Keep track of visited shapes while recursing to prevent infinite loops. + // TODO: Update type + let mut visited_shapes: HashMap = HashMap::new(); + + // Recursive function to call for (nested) shapes. + fn process_shape( + schema: &OrmSchema, + shape: &OrmSchemaShape, + subject_var_name: &str, + construct_statements: &mut Vec, + where_statements: &mut Vec, + var_counter: &mut i32, + visited_shapes: &mut HashMap, + max_recursion: u8, + ) { + // Prevent infinite recursion on cyclic schemas. + // Keep track of the number of shape occurrences and return if it's larger than max_recursion. + // For the last recursion, we could use by-reference queries but that could be for the future. + let current_self_recursion_depth = visited_shapes.get(&shape.iri).unwrap_or(&0); + if *current_self_recursion_depth > max_recursion { + return; + } else { + visited_shapes.insert(shape.iri.clone(), current_self_recursion_depth + 1); + } + + // Add statements for each predicate. + for predicate in &shape.predicates { + let mut union_branches = Vec::new(); + let mut allowed_literals = Vec::new(); + + // Predicate constraints might have more than one acceptable data type. Traverse each. + // It is assumed that constant literals, nested shapes and regular types are not mixed. + for datatype in &predicate.dataTypes { + if datatype.valType == OrmSchemaLiteralType::literal { + // Collect allowed literals and as strings + // (already in SPARQL-format, e.g. `"a astring"`, ``, `true`, or `42`). + allowed_literals.extend(literal_to_sparql_str(datatype.clone())); + } else if datatype.valType == OrmSchemaLiteralType::shape { + if let Some(shape_id) = &datatype.shape { + if let Some(nested_shape) = schema.get(shape_id) { + // For the current acceptable shape, add CONSTRUCT, WHERE, and recurse. + + // Each shape option gets its own var. + let obj_var_name = get_new_var_name(var_counter); + + construct_statements.push(format!( + " ?{} <{}> ?{}", + subject_var_name, predicate.iri, obj_var_name + )); + // Those are later added to a UNION, if there is more than one shape. + union_branches.push(format!( + " ?{} <{}> ?{}", + subject_var_name, predicate.iri, obj_var_name + )); + + // Recurse to add statements for nested object. + process_shape( + schema, + nested_shape, + &obj_var_name, + construct_statements, + where_statements, + var_counter, + visited_shapes, + max_recursion, + ); + } + } + } + } + + // The where statement which might be wrapped in OPTIONAL. + let where_body: String; + + if !allowed_literals.is_empty() + && !predicate.extra.unwrap_or(false) + && predicate.minCardinality > 0 + { + // If we have literal requirements and they are not optional ("extra"), + // Add CONSTRUCT, WHERE, and FILTER. + + let pred_var_name = get_new_var_name(var_counter); + construct_statements.push(format!( + " ?{} <{}> ?{}", + subject_var_name, predicate.iri, pred_var_name + )); + where_body = format!( + " ?{s} <{p}> ?{o} . \n FILTER (?{o} IN ({lits}))", + s = subject_var_name, + p = predicate.iri, + o = pred_var_name, + lits = allowed_literals.join(", ") + ); + } else if !union_branches.is_empty() { + // We have nested shape(s) which were already added to CONSTRUCT above. + // Join them with UNION. + + where_body = union_branches + .into_iter() + .map(|b| format!("{{\n{}\n}}", b)) + .collect::>() + .join(" UNION "); + } else { + // Regular predicate data type. Just add basic CONSTRUCT and WHERE statements. + + let pred_var_name = get_new_var_name(var_counter); + construct_statements.push(format!( + " ?{} <{}> ?{}", + subject_var_name, predicate.iri, pred_var_name + )); + where_body = format!( + " ?{} <{}> ?{}", + subject_var_name, predicate.iri, pred_var_name + ); + } + + // Wrap in optional, if necessary. + if predicate.minCardinality < 1 { + where_statements.push(format!(" OPTIONAL {{\n{}\n }}", where_body)); + } else { + where_statements.push(where_body); + }; + } + + visited_shapes.remove(&shape.iri); + } + + let root_shape = shape_type + .schema + .get(&shape_type.shape) + .ok_or(VerifierError::InvalidOrmSchema)?; + + // Root subject variable name + let root_var_name = get_new_var_name(&mut var_counter); + + process_shape( + &shape_type.schema, + root_shape, + &root_var_name, + &mut construct_statements, + &mut where_statements, + &mut var_counter, + &mut visited_shapes, + max_recursion.unwrap_or(1), + ); + + // Create query from statements. + let construct_body = construct_statements.join(" .\n"); + let where_body = where_statements.join(" .\n"); + Ok(format!( + "CONSTRUCT {{\n{}\n}}\nWHERE {{\n{}\n}}", + construct_body, where_body + )) +} + +// Escape an IRI fragment if needed (very conservative, only wrap with <...>). Assumes input already a full IRI. +fn escape_iri(iri: &str) -> String { + format!("<{}>", iri) +} + +// SPARQL literal escape: backslash, quotes, newlines, tabs. +fn escape_literal(lit: &str) -> String { + let mut out = String::with_capacity(lit.len() + 4); + for c in lit.chars() { + match c { + '\\' => out.push_str("\\\\"), + '\"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ => out.push(c), + } + } + return out; +} diff --git a/ng-verifier/src/request_processor.rs b/ng-verifier/src/request_processor.rs index 13c6d5e..0438567 100644 --- a/ng-verifier/src/request_processor.rs +++ b/ng-verifier/src/request_processor.rs @@ -49,16 +49,14 @@ impl Verifier { command: &AppRequestCommandV0, nuri: &NuriV0, _payload: &Option, - session_id: u64 + session_id: u64, ) -> Result<(Receiver, CancelFn), NgError> { match command { - AppRequestCommandV0::OrmStart => { - match _payload { - Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => { - self.start_orm(nuri, shape_type, session_id).await - }, - _ => return Err(NgError::InvalidArgument) + AppRequestCommandV0::OrmStart => match _payload { + Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => { + self.start_orm(nuri, shape_type, session_id).await } + _ => return Err(NgError::InvalidArgument), }, AppRequestCommandV0::Fetch(fetch) => match fetch { AppFetchContentV0::Subscribe => { @@ -162,8 +160,12 @@ impl Verifier { } } - pub(crate) async fn update_header(&mut self, target: &NuriTargetV0, title: Option, about: Option) -> Result<(), VerifierError> { - + pub(crate) async fn update_header( + &mut self, + target: &NuriTargetV0, + title: Option, + about: Option, + ) -> Result<(), VerifierError> { let (repo_id, branch_id, store_repo) = self.resolve_header_branch(target)?; let graph_name = NuriV0::branch_repo_graph_name( &branch_id, @@ -196,9 +198,7 @@ impl Verifier { ); } } - let query = format!( - "DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}" - ); + let query = format!("DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}"); let oxistore = self.graph_dataset.as_ref().unwrap(); @@ -212,13 +212,12 @@ impl Verifier { if inserts.is_empty() && removes.is_empty() { Ok(()) } else { - self - .prepare_sparql_update( - Vec::from_iter(inserts), - Vec::from_iter(removes), - self.get_peer_id_for_skolem(), - ) - .await?; + self.prepare_sparql_update( + Vec::from_iter(inserts), + Vec::from_iter(removes), + self.get_peer_id_for_skolem(), + ) + .await?; Ok(()) } } @@ -628,24 +627,26 @@ impl Verifier { destination: String, store_repo: Option, ) -> Result { - let class = BranchCrdt::from(crdt, class_name)?; - + let nuri = if store_repo.is_none() { NuriV0::new_private_store_target() } else { NuriV0::from_store_repo(&store_repo.unwrap()) }; - + let destination = DocCreateDestination::from(destination)?; - - self.doc_create(nuri, DocCreate { - class, - destination, - }).await + + self.doc_create(nuri, DocCreate { class, destination }) + .await } - pub(crate) async fn sparql_query(&self, nuri: &NuriV0, sparql: String, base: Option) -> Result { + pub(crate) async fn sparql_query( + &self, + nuri: &NuriV0, + sparql: String, + base: Option, + ) -> Result { //log_debug!("query={}", query); let store = self.graph_dataset.as_ref().unwrap(); let mut parsed = Query::parse(&sparql, base.as_deref()) @@ -658,27 +659,22 @@ impl Verifier { } store .query(parsed, self.resolve_target_for_sparql(&nuri.target, false)?) - .map_err(|e| VerifierError::SparqlError(e.to_string())) + .map_err(|e| VerifierError::SparqlError(e.to_string())) } pub(crate) async fn doc_create( &mut self, nuri: NuriV0, - doc_create: DocCreate + doc_create: DocCreate, ) -> Result { //TODO: deal with doc_create.destination let user_id = self.user_id().clone(); let user_priv_key = self.user_privkey().clone(); let primary_class = doc_create.class.class().clone(); - let (_,_,store) = self.resolve_target(&nuri.target)?; + let (_, _, store) = self.resolve_target(&nuri.target)?; let repo_id = self - .new_repo_default( - &user_id, - &user_priv_key, - &store, - doc_create.class, - ) + .new_repo_default(&user_id, &user_priv_key, &store, doc_create.class) .await?; let header_branch_id = { @@ -687,8 +683,7 @@ impl Verifier { }; // adding an AddRepo commit to the Store branch of store. - self.send_add_repo_to_store(&repo_id, &store) - .await?; + self.send_add_repo_to_store(&repo_id, &store).await?; // adding an ldp:contains triple to the store main branch let overlay_id = store.outer_overlay(); @@ -696,7 +691,9 @@ impl Verifier { let nuri_result = NuriV0::repo_graph_name(&repo_id, &overlay_id); let store_nuri = NuriV0::from_store_repo(&store); let store_nuri_string = NuriV0::repo_id(store.repo_id()); - let query = format!("INSERT DATA {{ <{store_nuri_string}> <{nuri}>. }}"); + let query = format!( + "INSERT DATA {{ <{store_nuri_string}> <{nuri}>. }}" + ); let ret = self .process_sparql_update(&store_nuri, &query, &None, vec![]) @@ -722,26 +719,35 @@ impl Verifier { Ok(nuri_result) } - fn get_profile_for_inbox_post(&self, public: bool) -> Result<(StoreRepo, PrivKey),NgError> { - + fn get_profile_for_inbox_post(&self, public: bool) -> Result<(StoreRepo, PrivKey), NgError> { let from_profile_id = if !public { self.config.protected_store_id.unwrap() } else { self.config.public_store_id.unwrap() }; - - let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?; + + let repo = self + .repos + .get(&from_profile_id) + .ok_or(NgError::RepoNotFound)?; let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?; let store_repo = repo.store.get_store_repo(); - Ok( (store_repo.clone(), inbox.clone()) ) + Ok((store_repo.clone(), inbox.clone())) } - async fn import_contact_from_qrcode(&mut self, repo_id: RepoId, contact: NgQRCodeProfileSharingV0) -> Result<(), VerifierError> { - + async fn import_contact_from_qrcode( + &mut self, + repo_id: RepoId, + contact: NgQRCodeProfileSharingV0, + ) -> Result<(), VerifierError> { let inbox_nuri_string: String = NuriV0::inbox(&contact.inbox); let profile_nuri_string: String = NuriV0::from_store_repo_string(&contact.profile); - let a_or_b = if contact.profile.is_public() { "site" } else { "protected" }; + let a_or_b = if contact.profile.is_public() { + "site" + } else { + "protected" + }; // checking if this contact has already been added match self.sparql_query( @@ -755,7 +761,8 @@ impl Verifier { } // getting the privkey of the inbox and ovelray because we will need it here below to send responses. - let (from_profile, from_inbox) = self.get_profile_for_inbox_post(contact.profile.is_public())?; + let (from_profile, from_inbox) = + self.get_profile_for_inbox_post(contact.profile.is_public())?; // get the name and optional email address of the profile we will respond with. // if we don't have a name, we fail @@ -787,26 +794,37 @@ impl Verifier { let contact_doc_nuri_string = NuriV0::repo_id(&repo_id); let contact_doc_nuri = NuriV0::new_repo_target_from_id(&repo_id); - let has_email = contact.email.map_or("".to_string(), |email| format!("<> vcard:hasEmail \"{email}\".")); + let has_email = contact.email.map_or("".to_string(), |email| { + format!("<> vcard:hasEmail \"{email}\".") + }); - let sparql_update = format!(" PREFIX ng: + let sparql_update = format!( + " PREFIX ng: PREFIX vcard: INSERT DATA {{ <> ng:{a_or_b} <{profile_nuri_string}>. <> ng:{a_or_b}_inbox <{inbox_nuri_string}>. <> a vcard:Individual . <> vcard:fn \"{}\". - {has_email} }}", contact.name); + {has_email} }}", + contact.name + ); let ret = self - .process_sparql_update(&contact_doc_nuri, &sparql_update, &Some(contact_doc_nuri_string), vec![]) + .process_sparql_update( + &contact_doc_nuri, + &sparql_update, + &Some(contact_doc_nuri_string), + vec![], + ) .await; if let Err(e) = ret { return Err(VerifierError::SparqlError(e)); } - self.update_header(&contact_doc_nuri.target, Some(contact.name), None).await?; + self.update_header(&contact_doc_nuri.target, Some(contact.name), None) + .await?; self.post_to_inbox(InboxPost::new_contact_details( - from_profile, + from_profile, from_inbox, contact.profile.outer_overlay(), contact.inbox, @@ -814,31 +832,43 @@ impl Verifier { false, name, email, - )?).await?; + )?) + .await?; Ok(()) } - pub(crate) async fn search_for_contacts(&self, excluding_profile_id_nuri: Option) -> Result, VerifierError> { + pub(crate) async fn search_for_contacts( + &self, + excluding_profile_id_nuri: Option, + ) -> Result, VerifierError> { let extra_conditions = if let Some(s) = excluding_profile_id_nuri { - format!("&& NOT EXISTS {{ ?c ng:site <{s}> }} && NOT EXISTS {{ ?c ng:protected <{s}> }}") + format!( + "&& NOT EXISTS {{ ?c ng:site <{s}> }} && NOT EXISTS {{ ?c ng:protected <{s}> }}" + ) } else { String::new() }; - let sparql = format!("PREFIX ng: + let sparql = format!( + "PREFIX ng: SELECT ?profile_id ?inbox_id WHERE {{ ?c a . OPTIONAL {{ ?c ng:site ?profile_id . ?c ng:site_inbox ?inbox_id }} OPTIONAL {{ ?c ng:protected ?profile_id . ?c ng:protected_inbox ?inbox_id }} FILTER ( bound(?profile_id) {extra_conditions} ) - }}"); + }}" + ); //log_info!("{sparql}"); - let sols = match self.sparql_query( - &NuriV0::new_entire_user_site(), - sparql, None).await? + let sols = match self + .sparql_query(&NuriV0::new_entire_user_site(), sparql, None) + .await? { - QueryResults::Solutions(sols) => { sols } - _ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())), + QueryResults::Solutions(sols) => sols, + _ => { + return Err(VerifierError::SparqlError( + NgError::InvalidResponse.to_string(), + )) + } }; let mut res = vec![]; @@ -857,8 +887,7 @@ impl Verifier { } } Ok(res) - -} + } pub(crate) async fn process( &mut self, @@ -867,30 +896,35 @@ impl Verifier { payload: Option, ) -> Result { match command { - AppRequestCommandV0::OrmUpdate => { - match payload { - Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff,shape_id)))) => { - self.frontend_update_orm(&nuri, shape_id, diff).await - }, - _ => return Err(NgError::InvalidArgument) + AppRequestCommandV0::OrmUpdate => match payload { + Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff, shape_id)))) => { + self.orm_frontend_update(&nuri, shape_id, diff).await } + _ => return Err(NgError::InvalidArgument), }, AppRequestCommandV0::SocialQueryStart => { - let (from_profile, contacts_string, degree) = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart{ - from_profile, contacts, degree - })) = - payload - { (from_profile, contacts, degree) } - else { - return Err(NgError::InvalidPayload); - }; + let (from_profile, contacts_string, degree) = + if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart { + from_profile, + contacts, + degree, + })) = payload + { + (from_profile, contacts, degree) + } else { + return Err(NgError::InvalidPayload); + }; let query_id = nuri.target.repo_id(); // checking that the query hasn't been started yet - match self.sparql_query( - &NuriV0::new_repo_target_from_id(query_id), - format!("ASK {{ <> ?forwarder }}"), Some(NuriV0::repo_id(query_id))).await? + match self + .sparql_query( + &NuriV0::new_repo_target_from_id(query_id), + format!("ASK {{ <> ?forwarder }}"), + Some(NuriV0::repo_id(query_id)), + ) + .await? { QueryResults::Boolean(true) => { return Err(NgError::SocialQueryAlreadyStarted); @@ -921,32 +955,36 @@ impl Verifier { //resolve from_profile let from_profile_id = match from_profile.target { - NuriTargetV0::ProtectedProfile => { - self.config.protected_store_id.unwrap() - } - NuriTargetV0::PublicProfile => { - self.config.public_store_id.unwrap() - }, - _ => return Err(NgError::InvalidNuri) + NuriTargetV0::ProtectedProfile => self.config.protected_store_id.unwrap(), + NuriTargetV0::PublicProfile => self.config.public_store_id.unwrap(), + _ => return Err(NgError::InvalidNuri), }; let store = { - let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?; + let repo = self + .repos + .get(&from_profile_id) + .ok_or(NgError::RepoNotFound)?; repo.store.clone() }; - + let definition_commit_body_ref = nuri.get_first_commit_ref()?; - let block_ids = Commit::collect_block_ids(definition_commit_body_ref.clone(), &store, true)?; - let mut blocks= Vec::with_capacity(block_ids.len()); + let block_ids = + Commit::collect_block_ids(definition_commit_body_ref.clone(), &store, true)?; + let mut blocks = Vec::with_capacity(block_ids.len()); //log_info!("blocks nbr {}",block_ids.len()); for bid in block_ids.iter() { blocks.push(store.get(bid)?); } // creating the ForwardedSocialQuery in the private store - let forwarder = self.doc_create_with_store_repo( - "Graph".to_string(), "social:query:forwarded".to_string(), - "store".to_string(), None // meaning in private store - ).await?; + let forwarder = self + .doc_create_with_store_repo( + "Graph".to_string(), + "social:query:forwarded".to_string(), + "store".to_string(), + None, // meaning in private store + ) + .await?; let forwarder_nuri = NuriV0::new_from_repo_graph(&forwarder)?; let forwarder_id = forwarder_nuri.target.repo_id().clone(); let forwarder_nuri_string = NuriV0::repo_id(&forwarder_id); @@ -965,28 +1003,36 @@ impl Verifier { let sparql_update = format!("INSERT DATA {{ <> <{social_query_doc_nuri_string}> . <> \"{}\"^^ . }}",DateTime::now()); let ret = self - .process_sparql_update(&forwarder_nuri, &sparql_update, &Some(forwarder_nuri_string), vec![]) + .process_sparql_update( + &forwarder_nuri, + &sparql_update, + &Some(forwarder_nuri_string), + vec![], + ) .await; if let Err(e) = ret { log_err!("{sparql_update}"); return Err(NgError::SparqlError(e)); } - let from_profiles: ((StoreRepo, PrivKey), (StoreRepo, PrivKey)) = self.get_2_profiles()?; + let from_profiles: ((StoreRepo, PrivKey), (StoreRepo, PrivKey)) = + self.get_2_profiles()?; for (to_profile_nuri, to_inbox_nuri) in contacts { - - match self.social_query_dispatch( - &to_profile_nuri, - &to_inbox_nuri, - &forwarder_nuri, - &forwarder_id, - &from_profiles, - query_id, - &definition_commit_body_ref, - &blocks, - degree - ).await { + match self + .social_query_dispatch( + &to_profile_nuri, + &to_inbox_nuri, + &forwarder_nuri, + &forwarder_id, + &from_profiles, + query_id, + &definition_commit_body_ref, + &blocks, + degree, + ) + .await + { Ok(_) => {} Err(e) => return Ok(AppResponse::error(e.to_string())), } @@ -1017,32 +1063,34 @@ impl Verifier { // }; } AppRequestCommandV0::QrCodeProfile => { - let size = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) = - payload - { - size - } else { - return Err(NgError::InvalidPayload); - }; + let size = + if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) = + payload + { + size + } else { + return Err(NgError::InvalidPayload); + }; let public = match nuri.target { NuriTargetV0::PublicProfile => true, NuriTargetV0::ProtectedProfile => false, - _ => return Err(NgError::InvalidPayload) + _ => return Err(NgError::InvalidPayload), }; return match self.get_qrcode_for_profile(public, size).await { Err(e) => Ok(AppResponse::error(e.to_string())), - Ok(qrcode) => Ok(AppResponse::text(qrcode)), + Ok(qrcode) => Ok(AppResponse::text(qrcode)), }; } AppRequestCommandV0::QrCodeProfileImport => { - let profile = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfileImport( text))) = - payload + let profile = if let Some(AppRequestPayload::V0( + AppRequestPayloadV0::QrCodeProfileImport(text), + )) = payload { let ser = base64_url::decode(&text).map_err(|_| NgError::SerializationError)?; - let code:NgQRCode = serde_bare::from_slice(&ser)?; + let code: NgQRCode = serde_bare::from_slice(&ser)?; let profile = match code { NgQRCode::ProfileSharingV0(profile) => profile, - _ => return Err(NgError::InvalidPayload) + _ => return Err(NgError::InvalidPayload), }; profile } else { @@ -1050,20 +1098,23 @@ impl Verifier { }; let repo_id = match nuri.target { NuriTargetV0::Repo(id) => id, - _ => return Err(NgError::InvalidPayload) + _ => return Err(NgError::InvalidPayload), }; return match self.import_contact_from_qrcode(repo_id, profile).await { Err(e) => Ok(AppResponse::error(e.to_string())), - Ok(()) => Ok(AppResponse::ok()), + Ok(()) => Ok(AppResponse::ok()), }; } AppRequestCommandV0::Header => { if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Header(doc_header))) = payload { - return match self.update_header(&nuri.target, doc_header.title, doc_header.about).await { + return match self + .update_header(&nuri.target, doc_header.title, doc_header.about) + .await + { Ok(_) => Ok(AppResponse::ok()), - Err(e) => Ok(AppResponse::error(e.to_string())) + Err(e) => Ok(AppResponse::error(e.to_string())), }; } else { return Err(NgError::InvalidPayload); @@ -1076,7 +1127,7 @@ impl Verifier { match self.doc_create(nuri, doc_create).await { Err(NgError::SparqlError(e)) => Ok(AppResponse::error(e)), Err(e) => Err(e), - Ok(nuri_result) => Ok(AppResponse::V0(AppResponseV0::Nuri(nuri_result))) + Ok(nuri_result) => Ok(AppResponse::V0(AppResponseV0::Nuri(nuri_result))), } } else { Err(NgError::InvalidPayload) @@ -1250,15 +1301,16 @@ impl Verifier { return Ok(AppResponse::V0(AppResponseV0::Text(vec.join("\n")))); } AppFetchContentV0::CurrentHeads => { - if nuri.target.is_repo_id() { - if let Ok(s) = self.get_main_branch_current_heads_nuri(nuri.target.repo_id()) { + if let Ok(s) = + self.get_main_branch_current_heads_nuri(nuri.target.repo_id()) + { return Ok(AppResponse::V0(AppResponseV0::Text(s))); } } return Ok(AppResponse::error(VerifierError::InvalidNuri.to_string())); } - + AppFetchContentV0::History => { if !nuri.is_valid_for_sparql_update() { return Err(NgError::InvalidNuri); diff --git a/ng-verifier/src/verifier.rs b/ng-verifier/src/verifier.rs index 964269b..e9da845 100644 --- a/ng-verifier/src/verifier.rs +++ b/ng-verifier/src/verifier.rs @@ -25,6 +25,7 @@ use async_std::stream::StreamExt; use async_std::sync::{Mutex, RwLockReadGuard}; use futures::channel::mpsc; use futures::SinkExt; +use ng_net::orm::OrmShapeTypeRef; use ng_oxigraph::oxigraph::sparql::Query; use ng_oxigraph::oxigraph::sparql::QueryResults; use ng_oxigraph::oxrdf::Term; @@ -111,7 +112,9 @@ pub struct Verifier { in_memory_outbox: Vec, uploads: BTreeMap, branch_subscriptions: HashMap>, - pub(crate) orm_subscriptions: HashMap>>>, + pub(crate) orm_subscriptions: + HashMap>>>, + pub(crate) orm_shape_types: HashMap, pub(crate) temporary_repo_certificates: HashMap, } @@ -518,6 +521,7 @@ impl Verifier { uploads: BTreeMap::new(), branch_subscriptions: HashMap::new(), orm_subscriptions: HashMap::new(), + orm_shape_types: HashMap::new(), temporary_repo_certificates: HashMap::new(), } } @@ -1288,9 +1292,9 @@ impl Verifier { // registering inbox for protected and public store. (FIXME: this should be done instead in the 1st connection during wallet creation) let remote = self.connected_broker.connected_or_err()?; let mut done = false; - for (_,store) in self.stores.iter() { + for (_, store) in self.stores.iter() { if store.id() == self.protected_store_id() || store.id() == self.public_store_id() { - let repo = self.get_repo( store.id(), &store.get_store_repo())?; + let repo = self.get_repo(store.id(), &store.get_store_repo())?; let inbox = repo.inbox.to_owned().unwrap(); // sending InboxRegister let msg = InboxRegister::new(inbox, store.outer_overlay())?; @@ -1568,8 +1572,11 @@ impl Verifier { Ok(()) } - pub async fn get_qrcode_for_profile(&self, public: bool, size: u32) -> Result { - + pub async fn get_qrcode_for_profile( + &self, + public: bool, + size: u32, + ) -> Result { let profile_id = if public { self.public_store_id() } else { @@ -1577,21 +1584,31 @@ impl Verifier { }; let repo = self.repos.get(&profile_id).ok_or(NgError::RepoNotFound)?; - let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?.to_pub(); + let inbox = repo + .inbox + .to_owned() + .ok_or(NgError::InboxNotFound)? + .to_pub(); let profile = repo.store.get_store_repo().clone(); - let sparql = format!(" + let sparql = format!( + " PREFIX vcard: SELECT ?name ?email WHERE {{ <> vcard:fn ?name . <> vcard:hasEmail ?email . - }}"); + }}" + ); //log_info!("{sparql}"); - let (name, email) = match self.sparql_query( - &NuriV0::new_repo_target_from_id(profile_id), - sparql, Some(NuriV0::repo_id(profile_id))).await? + let (name, email) = match self + .sparql_query( + &NuriV0::new_repo_target_from_id(profile_id), + sparql, + Some(NuriV0::repo_id(profile_id)), + ) + .await? { - QueryResults::Solutions(mut sols) => { + QueryResults::Solutions(mut sols) => { match sols.next() { None => { //log_info!("name or email not found"); @@ -1613,39 +1630,39 @@ impl Verifier { }; (name, email) } - } + } + } + _ => { + return Err(VerifierError::SparqlError( + NgError::InvalidResponse.to_string(), + )) } - _ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())), }; if name.is_none() { return Err(VerifierError::InvalidProfile); } - let profile_sharing = NgQRCode::ProfileSharingV0(NgQRCodeProfileSharingV0 { - inbox, - profile, - name: name.unwrap(), - email + let profile_sharing = NgQRCode::ProfileSharingV0(NgQRCodeProfileSharingV0 { + inbox, + profile, + name: name.unwrap(), + email, }); let ser = serde_bare::to_vec(&profile_sharing)?; let encoded = base64_url::encode(&ser); log_info!("qrcode= {encoded}"); match QrCode::with_error_correction_level(encoded.as_bytes(), qrcode::EcLevel::M) { - Ok(qr) => { - Ok(qr - .render() - .max_dimensions(size, size) - .dark_color(svg::Color("#000000")) - .light_color(svg::Color("#ffffff")) - .build() - ) - } + Ok(qr) => Ok(qr + .render() + .max_dimensions(size, size) + .dark_color(svg::Color("#000000")) + .light_color(svg::Color("#ffffff")) + .build()), Err(e) => Err(VerifierError::QrCode(e.to_string())), } } pub async fn inbox(&mut self, msg: &InboxMsg, from_queue: bool) { - //log_info!("RECEIVED INBOX MSG {:?}", msg); match self.inboxes.get(&msg.body.to_inbox) { @@ -1660,27 +1677,31 @@ impl Verifier { if let Err(e) = res { log_err!("Error during process_inbox {e}"); } - }, + } Err(e) => { log_err!("cannot unseal inbox msg {e}"); } } } - }, + } None => {} } - }, + } None => {} } - if from_queue && self.connected_broker.is_some(){ + if from_queue && self.connected_broker.is_some() { log_info!("try to pop one more inbox msg"); // try to pop inbox msg let connected_broker = self.connected_broker.clone(); let broker = BROKER.read().await; let user = self.user_id().clone(); let _ = broker - .send_client_event(&Some(user), &connected_broker.into(), ClientEvent::InboxPopRequest) + .send_client_event( + &Some(user), + &connected_broker.into(), + ClientEvent::InboxPopRequest, + ) .await; } } @@ -1794,12 +1815,15 @@ impl Verifier { } } - pub(crate) fn get_main_branch_current_heads_nuri(&self, repo_id: &RepoId) -> Result { + pub(crate) fn get_main_branch_current_heads_nuri( + &self, + repo_id: &RepoId, + ) -> Result { if let Some(repo) = self.repos.get(repo_id) { if let Some(info) = repo.main_branch() { let mut res = NuriV0::repo_id(repo_id); for head in info.current_heads.iter() { - res = [res,NuriV0::commit_ref(head)].join(":"); + res = [res, NuriV0::commit_ref(head)].join(":"); } return Ok(res); } @@ -1886,14 +1910,22 @@ impl Verifier { let storage = match self.repos.get_mut(&inbox_cap.repo_id) { Some(repo) => { repo.inbox = Some(inbox_cap.priv_key.clone()); - log_info!("INBOX for {} : {}", inbox_cap.repo_id.to_string(), inbox_cap.priv_key.to_pub().to_string()); + log_info!( + "INBOX for {} : {}", + inbox_cap.repo_id.to_string(), + inbox_cap.priv_key.to_pub().to_string() + ); self.inboxes.insert(inbox_cap.priv_key.to_pub(), repo.id); self.user_storage_if_persistent() } None => self.user_storage(), }; if let Some(user_storage) = storage { - user_storage.update_inbox_cap(&inbox_cap.repo_id, &inbox_cap.overlay, &inbox_cap.priv_key)?; + user_storage.update_inbox_cap( + &inbox_cap.repo_id, + &inbox_cap.overlay, + &inbox_cap.priv_key, + )?; } Ok(()) @@ -2782,6 +2814,7 @@ impl Verifier { uploads: BTreeMap::new(), branch_subscriptions: HashMap::new(), orm_subscriptions: HashMap::new(), + orm_shape_types: HashMap::new(), temporary_repo_certificates: HashMap::new(), }; // this is important as it will load the last seq from storage @@ -2839,8 +2872,14 @@ impl Verifier { //self.populate_topics(&repo); let _ = self.add_doc(&repo.id, &repo.store.overlay_id); if repo.inbox.is_some() { - log_info!("INBOX for {} : {}", repo.id.to_string(), repo.inbox.as_ref().unwrap().to_pub().to_string()); - _ = self.inboxes.insert(repo.inbox.as_ref().unwrap().to_pub(), repo.id); + log_info!( + "INBOX for {} : {}", + repo.id.to_string(), + repo.inbox.as_ref().unwrap().to_pub().to_string() + ); + _ = self + .inboxes + .insert(repo.inbox.as_ref().unwrap().to_pub(), repo.id); } let repo_ref = self.repos.entry(repo.id).or_insert(repo); repo_ref diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts index 94d1f54..34a1f0b 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts @@ -82,7 +82,7 @@ function addConstructPattern( (pred.minCardinality ?? 0) === 0 && (options?.includeOptionalForMinZero ?? true); - if (pred.valType === "nested" && pred.nestedShape) { + if (pred.dataTypes === "nested" && pred.nestedShape) { template.push(triple); const nestedBody: string[] = [triple]; const nestedPreds = pred.nestedShape.predicates; @@ -108,7 +108,7 @@ function addConstructPattern( template.push(triple); const blockLines: string[] = [triple]; - if (pred.valType === "literal" && pred.literalValue !== undefined) { + if (pred.dataTypes === "literal" && pred.literalValue !== undefined) { if (Array.isArray(pred.literalValue)) { valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[])); } else { diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts index 7f9ae6b..98e932d 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts @@ -35,7 +35,7 @@ export const buildConstructQuery = ({ for (const pred of predicates) { const subjectVarName = getVarNameFor(shapeId); - if (pred.valType === "nested") { + if (pred.dataTypes === "nested") { if (typeof pred.nestedShape !== "string") throw new Error("Nested shapes must be by reference"); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts index 63b1040..ab13bb9 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts @@ -10,7 +10,7 @@ export const testShapeSchema: Schema = { iri: "http://example.org/TestObject", predicates: [ { - valType: "literal", + dataTypes: "literal", literalValue: ["TestObject"], maxCardinality: 1, minCardinality: 1, @@ -19,35 +19,35 @@ export const testShapeSchema: Schema = { extra: true, }, { - valType: "string", + dataTypes: "string", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/stringValue", readablePredicate: "stringValue", }, { - valType: "number", + dataTypes: "number", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/numValue", readablePredicate: "numValue", }, { - valType: "boolean", + dataTypes: "boolean", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/boolValue", readablePredicate: "boolValue", }, { - valType: "number", + dataTypes: "number", maxCardinality: -1, minCardinality: 0, iri: "http://example.org/arrayValue", readablePredicate: "arrayValue", }, { - valType: "nested", + dataTypes: "nested", nestedShape: "http://example.org/TestObject||http://example.org/objectValue", maxCardinality: 1, @@ -56,7 +56,7 @@ export const testShapeSchema: Schema = { readablePredicate: "objectValue", }, { - valType: "nested", + dataTypes: "nested", nestedShape: "http://example.org/TestObject||http://example.org/anotherObject", maxCardinality: -1, @@ -65,7 +65,7 @@ export const testShapeSchema: Schema = { readablePredicate: "anotherObject", }, { - valType: "eitherOf", + dataTypes: "eitherOf", eitherOf: [ { valType: "string", @@ -85,21 +85,21 @@ export const testShapeSchema: Schema = { iri: "http://example.org/TestObject||http://example.org/objectValue", predicates: [ { - valType: "string", + dataTypes: "string", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/nestedString", readablePredicate: "nestedString", }, { - valType: "number", + dataTypes: "number", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/nestedNum", readablePredicate: "nestedNum", }, { - valType: "number", + dataTypes: "number", maxCardinality: -1, minCardinality: 0, iri: "http://example.org/nestedArray", @@ -111,14 +111,14 @@ export const testShapeSchema: Schema = { iri: "http://example.org/TestObject||http://example.org/anotherObject", predicates: [ { - valType: "string", + dataTypes: "string", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/prop1", readablePredicate: "prop1", }, { - valType: "number", + dataTypes: "number", maxCardinality: 1, minCardinality: 1, iri: "http://example.org/prop2", diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.ts index 0f38637..e25769f 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.ts @@ -6,73 +6,104 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm"; * ============================================================================= */ export const catShapeSchema: Schema = { - "http://example.org/Cat": { - iri: "http://example.org/Cat", - predicates: [ - { - valType: "literal", - literalValue: ["http://example.org/Cat"], - maxCardinality: 1, - minCardinality: 1, - iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", - readablePredicate: "type", - }, - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/name", - readablePredicate: "name", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/age", - readablePredicate: "age", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/numberOfHomes", - readablePredicate: "numberOfHomes", - }, - { - valType: "nested", - nestedShape: - "http://example.org/Cat||http://example.org/address", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/address", - readablePredicate: "address", - }, + "http://example.org/Cat": { + iri: "http://example.org/Cat", + predicates: [ + { + dataTypes: [ + { + valType: "literal", + literals: ["http://example.org/Cat"], + }, ], - }, - "http://example.org/Cat||http://example.org/address": { - iri: "http://example.org/Cat||http://example.org/address", - predicates: [ - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/street", - readablePredicate: "street", - }, - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/houseNumber", - readablePredicate: "houseNumber", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/floor", - readablePredicate: "floor", - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + readablePredicate: "type", + }, + { + dataTypes: [ + { + valType: "string", + }, ], - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/name", + readablePredicate: "name", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/age", + readablePredicate: "age", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/numberOfHomes", + readablePredicate: "numberOfHomes", + }, + { + dataTypes: [ + { + valType: "shape", + shape: "http://example.org/Cat||http://example.org/address", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/address", + readablePredicate: "address", + }, + ], + }, + "http://example.org/Cat||http://example.org/address": { + iri: "http://example.org/Cat||http://example.org/address", + predicates: [ + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/street", + readablePredicate: "street", + }, + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/houseNumber", + readablePredicate: "houseNumber", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/floor", + readablePredicate: "floor", + }, + ], + }, }; diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.ts index 4975beb..5748e65 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.ts @@ -6,66 +6,93 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm"; * ============================================================================= */ export const personShapeSchema: Schema = { - "http://example.org/Person": { - iri: "http://example.org/Person", - predicates: [ - { - valType: "literal", - literalValue: ["http://example.org/Person"], - maxCardinality: 1, - minCardinality: 1, - iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", - readablePredicate: "type", - }, - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/name", - readablePredicate: "name", - }, - { - valType: "nested", - nestedShape: - "http://example.org/Person||http://example.org/address", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/address", - readablePredicate: "address", - }, - { - valType: "boolean", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/hasChildren", - readablePredicate: "hasChildren", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/numberOfHouses", - readablePredicate: "numberOfHouses", - }, + "http://example.org/Person": { + iri: "http://example.org/Person", + predicates: [ + { + dataTypes: [ + { + valType: "literal", + literals: ["http://example.org/Person"], + }, ], - }, - "http://example.org/Person||http://example.org/address": { - iri: "http://example.org/Person||http://example.org/address", - predicates: [ - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/street", - readablePredicate: "street", - }, - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/houseNumber", - readablePredicate: "houseNumber", - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + readablePredicate: "type", + }, + { + dataTypes: [ + { + valType: "string", + }, ], - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/name", + readablePredicate: "name", + }, + { + dataTypes: [ + { + valType: "shape", + shape: "http://example.org/Person||http://example.org/address", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/address", + readablePredicate: "address", + }, + { + dataTypes: [ + { + valType: "boolean", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/hasChildren", + readablePredicate: "hasChildren", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/numberOfHouses", + readablePredicate: "numberOfHouses", + }, + ], + }, + "http://example.org/Person||http://example.org/address": { + iri: "http://example.org/Person||http://example.org/address", + predicates: [ + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/street", + readablePredicate: "street", + }, + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/houseNumber", + readablePredicate: "houseNumber", + }, + ], + }, }; diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.ts index b882b01..64d65d1 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.ts @@ -6,132 +6,183 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm"; * ============================================================================= */ export const testShapeSchema: Schema = { - "http://example.org/TestObject": { - iri: "http://example.org/TestObject", - predicates: [ - { - valType: "literal", - literalValue: ["http://example.org/TestObject"], - maxCardinality: 1, - minCardinality: 1, - iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", - readablePredicate: "type", - extra: true, - }, - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/stringValue", - readablePredicate: "stringValue", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/numValue", - readablePredicate: "numValue", - }, - { - valType: "boolean", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/boolValue", - readablePredicate: "boolValue", - }, - { - valType: "number", - maxCardinality: -1, - minCardinality: 0, - iri: "http://example.org/arrayValue", - readablePredicate: "arrayValue", - }, - { - valType: "nested", - nestedShape: - "http://example.org/TestObject||http://example.org/objectValue", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/objectValue", - readablePredicate: "objectValue", - }, - { - valType: "nested", - nestedShape: - "http://example.org/TestObject||http://example.org/anotherObject", - maxCardinality: -1, - minCardinality: 0, - iri: "http://example.org/anotherObject", - readablePredicate: "anotherObject", - }, - { - valType: "eitherOf", - eitherOf: [ - { - valType: "string", - }, - { - valType: "number", - }, - ], - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/numOrStr", - readablePredicate: "numOrStr", - }, - { - valType: "literal", - literalValue: ["lit1", "lit2"], - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/lit1Or2", - readablePredicate: "lit1Or2", - }, + "http://example.org/TestObject": { + iri: "http://example.org/TestObject", + predicates: [ + { + dataTypes: [ + { + valType: "literal", + literals: ["http://example.org/TestObject"], + }, ], - }, - "http://example.org/TestObject||http://example.org/objectValue": { - iri: "http://example.org/TestObject||http://example.org/objectValue", - predicates: [ - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/nestedString", - readablePredicate: "nestedString", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/nestedNum", - readablePredicate: "nestedNum", - }, - { - valType: "number", - maxCardinality: -1, - minCardinality: 0, - iri: "http://example.org/nestedArray", - readablePredicate: "nestedArray", - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + readablePredicate: "type", + extra: true, + }, + { + dataTypes: [ + { + valType: "string", + }, ], - }, - "http://example.org/TestObject||http://example.org/anotherObject": { - iri: "http://example.org/TestObject||http://example.org/anotherObject", - predicates: [ - { - valType: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/prop1", - readablePredicate: "prop1", - }, - { - valType: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/prop2", - readablePredicate: "prop2", - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/stringValue", + readablePredicate: "stringValue", + }, + { + dataTypes: [ + { + valType: "number", + }, ], - }, + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/numValue", + readablePredicate: "numValue", + }, + { + dataTypes: [ + { + valType: "boolean", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/boolValue", + readablePredicate: "boolValue", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: -1, + minCardinality: 0, + iri: "http://example.org/arrayValue", + readablePredicate: "arrayValue", + }, + { + dataTypes: [ + { + valType: "shape", + shape: + "http://example.org/TestObject||http://example.org/objectValue", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/objectValue", + readablePredicate: "objectValue", + }, + { + dataTypes: [ + { + valType: "shape", + shape: + "http://example.org/TestObject||http://example.org/anotherObject", + }, + ], + maxCardinality: -1, + minCardinality: 0, + iri: "http://example.org/anotherObject", + readablePredicate: "anotherObject", + }, + { + dataTypes: [ + { + valType: "string", + }, + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/numOrStr", + readablePredicate: "numOrStr", + }, + { + dataTypes: [ + { + valType: "literal", + literals: ["lit1", "lit2"], + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/lit1Or2", + readablePredicate: "lit1Or2", + }, + ], + }, + "http://example.org/TestObject||http://example.org/objectValue": { + iri: "http://example.org/TestObject||http://example.org/objectValue", + predicates: [ + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/nestedString", + readablePredicate: "nestedString", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/nestedNum", + readablePredicate: "nestedNum", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: -1, + minCardinality: 0, + iri: "http://example.org/nestedArray", + readablePredicate: "nestedArray", + }, + ], + }, + "http://example.org/TestObject||http://example.org/anotherObject": { + iri: "http://example.org/TestObject||http://example.org/anotherObject", + predicates: [ + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/prop1", + readablePredicate: "prop1", + }, + { + dataTypes: [ + { + valType: "number", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/prop2", + readablePredicate: "prop2", + }, + ], + }, }; diff --git a/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/converter.ts b/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/converter.ts index a853332..99a731e 100644 --- a/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/converter.ts +++ b/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/converter.ts @@ -77,27 +77,38 @@ function flattenSchema(shapes: Shape[]): ShapeSchema { for (const shape of shapes) { schema[shape.iri] = shape; - // Find nested, unflattened (i.e. anonymous) schemas in properties. - const nestedSchemaPredicates = shape.predicates.filter( - (pred) => - pred.valType === "nested" && - typeof pred.nestedShape === "object" - ); - - for (const pred of nestedSchemaPredicates) { - const newId = shape.iri + "||" + pred.iri; + // Find nested, unflattened (i.e. anonymous) schemas in predicates' dataTypes. + for (const pred of shape.predicates) { + for (let i = 0; i < pred.dataTypes.length; i++) { + const dt = pred.dataTypes[i]; + if ( + dt.valType === "shape" && + typeof dt.shape === "object" && + dt.shape !== null + ) { + // create a deterministic id for the nested shape; include index if multiple shape entries exist + const shapeCount = pred.dataTypes.filter( + (d) => d.valType === "shape" + ).length; + const newId = + shape.iri + + "||" + + pred.iri + + (shapeCount > 1 ? `||${i}` : ""); - // Recurse - const flattened = flattenSchema([ - { - ...(pred.nestedShape as Shape), - iri: newId, - }, - ]); - // Replace the nested schema with its new id. - pred.nestedShape = newId; + // Recurse + const flattened = flattenSchema([ + { + ...(dt.shape as Shape), + iri: newId, + }, + ]); + // Replace the nested schema with its new id. + dt.shape = newId; - schema = { ...schema, ...flattened }; + schema = { ...schema, ...flattened }; + } + } } // Flatten / Recurse } diff --git a/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJSchemaTransformer.ts b/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJSchemaTransformer.ts index 43f01a8..506b6ea 100644 --- a/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJSchemaTransformer.ts +++ b/sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJSchemaTransformer.ts @@ -65,7 +65,7 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer< EachOf: { return: Shape }; TripleConstraint: { return: Predicate }; NodeConstraint: { return: DataType }; - ShapeOr: { return: (DataType | Shape | string)[] }; + ShapeOr: { return: DataType[] }; ShapeAnd: { return: never }; ShapeNot: { return: never }; ShapeExternal: { return: never }; @@ -143,24 +143,31 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer< if (typeof transformedChildren.valueExpr === "string") { // Reference to nested object return { - valType: "nested", - nestedShape: transformedChildren.valueExpr, + dataTypes: [ + { + valType: "shape", + shape: transformedChildren.valueExpr, + }, + ], ...commonProperties, - } satisfies Predicate; + }; } else if ( transformedChildren.valueExpr && (transformedChildren.valueExpr as Shape).predicates ) { // Nested object return { - valType: "nested", - nestedShape: transformedChildren.valueExpr as Shape, + dataTypes: [ + { + valType: "shape", + shape: transformedChildren.valueExpr as Shape, + }, + ], ...commonProperties, - } satisfies Predicate; + }; } else if (Array.isArray(transformedChildren.valueExpr)) { return { - valType: "eitherOf", - eitherOf: transformedChildren.valueExpr, + dataTypes: transformedChildren.valueExpr, // DataType[] ...commonProperties, }; } else { @@ -168,10 +175,14 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer< const nodeConstraint = transformedChildren.valueExpr as DataType; return { - valType: nodeConstraint.valType, - literalValue: nodeConstraint.literals, + dataTypes: [ + { + valType: nodeConstraint.valType, + literals: nodeConstraint.literals, + }, + ], ...commonProperties, - } satisfies Predicate; + }; } }, }, @@ -192,6 +203,8 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer< valType: "literal", literals: nodeConstraint.values.map( // TODO: We do not convert them to number or boolean or lang tag. + // And we don't have an annotation of the literal's type. + // @ts-expect-error (valueRecord) => valueRecord.value || valueRecord.id ), }; @@ -210,11 +223,9 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer< transformer: async (shapeOr, getTransformedChildren) => { const { shapeExprs } = await getTransformedChildren(); // Either a shape IRI, a nested shape or a node CompactSchemaValue (node constraint). - return (Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs]) as ( - | string - | Shape - | DataType - )[]; + return ( + Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs] + ) as DataType[]; }, }, diff --git a/sdk/ng-sdk-js/ng-shex-orm/src/types.ts b/sdk/ng-sdk-js/ng-shex-orm/src/types.ts index 35f1c3f..fa044bc 100644 --- a/sdk/ng-sdk-js/ng-shex-orm/src/types.ts +++ b/sdk/ng-sdk-js/ng-shex-orm/src/types.ts @@ -17,27 +17,25 @@ export interface Shape { } export type DataType = { + /** The required literal value(s), if type is `literal`. Others are allowed, if `extra` is true. */ literals?: number[] | string[] | boolean; - valType: "number" | "string" | "boolean" | "iri" | "literal"; + /** If `valType` is `"shape"`, the nested shape or its reference. Use reference for serialization. */ + shape?: string | Shape; + /** The type of object value for a triple constraint. */ + valType: "number" | "string" | "boolean" | "iri" | "literal" | "shape"; }; export interface Predicate { - /** Type of property. */ - valType: DataType["valType"] | "nested" | "eitherOf"; + /** Allowed type of object. If more than one is present, either of them is allowed. */ + dataTypes: DataType[]; /** The RDF predicate URI. */ iri: string; /** The alias of the `predicateUri` when serialized to a JSON object. */ readablePredicate: string; - /** The required literal value(s), if type is `literal`. Others are allowed, if `extra` is true. */ - literalValue?: number | string | boolean | number[] | string[]; // TODO: We could live without this and use eitherOf instead... - /** If type is `nested`, the shape or its IRI. */ - nestedShape?: string | Shape; // TODO: Only allow Shape while parsing from traverser. We flatten afterwards. /** Maximum allowed number of values. `-1` means infinite. */ maxCardinality: number; /** Minimum required number of values */ minCardinality: number; - /** If type is `eitherOf`, specifies multiple allowed types (CompactSchemaValue, shapes, or shape IRI). */ - eitherOf?: (DataType | Shape | string)[]; // TODO: Shape is going to be by reference. /** If other (additional) values are permitted. Useful for literals. */ extra?: boolean; }