use std::collections::{HashMap, HashSet};
use crate::sbol2_vocab as v2;
use crate::vocab as v3;
use crate::{Iri, Resource, Term, Triple};
use sbol_rdf::{Graph, ParseError, RdfFormat};
mod emit;
mod engine;
mod identity;
mod values;
use identity::IdentityMap;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct UpgradeOptions {
pub default_namespace: Option<Iri>,
pub preserve_backport: bool,
}
impl Default for UpgradeOptions {
fn default() -> Self {
Self {
default_namespace: None,
preserve_backport: true,
}
}
}
impl UpgradeOptions {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpgradeWarning {
NamespaceFallback {
subject: String,
source: NamespaceSource,
},
UnresolvedMapsTo { mapsto: String, side: MapsToSide },
UnsupportedRefinement { mapsto: String, refinement: String },
SequenceAnnotationWithComponent { annotation: String },
UnknownSbol2Type { subject: String, sbol2_type: String },
LocationWithoutSequence {
location: String,
component: String,
sequence_count: usize,
},
IdentityCollision {
canonical: String,
sources: Vec<String>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NamespaceSource {
UrlOrigin,
DefaultOption,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MapsToSide {
Local,
Remote,
Carrier,
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct UpgradeCounts {
pub component_definitions: usize,
pub module_definitions: usize,
pub sub_components: usize,
pub sequence_features: usize,
pub sequence_annotations_collapsed: usize,
pub mapstos_decomposed: usize,
pub interfaces_synthesized: usize,
pub locations_with_inferred_sequence: usize,
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct UpgradeReport {
warnings: Vec<UpgradeWarning>,
counts: UpgradeCounts,
}
impl UpgradeReport {
pub fn warnings(&self) -> &[UpgradeWarning] {
&self.warnings
}
pub fn counts(&self) -> &UpgradeCounts {
&self.counts
}
pub fn is_clean(&self) -> bool {
self.warnings.is_empty()
}
pub(crate) fn push(&mut self, warning: UpgradeWarning) {
self.warnings.push(warning);
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum UpgradeError {
Parse(ParseError),
NotSbol2,
}
impl std::fmt::Display for UpgradeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(err) => write!(f, "failed to parse input as RDF: {err}"),
Self::NotSbol2 => write!(
f,
"input contains no SBOL 2 typed subjects \
(http://sbols.org/v2# rdf:type) — nothing to upgrade",
),
}
}
}
impl std::error::Error for UpgradeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(err) => Some(err),
Self::NotSbol2 => None,
}
}
}
impl From<ParseError> for UpgradeError {
fn from(err: ParseError) -> Self {
Self::Parse(err)
}
}
pub fn sbol2_to_sbol3(
graph: &Graph,
options: UpgradeOptions,
) -> Result<(Graph, UpgradeReport), UpgradeError> {
let mut engine = Engine::new(graph, options);
engine.preflight()?;
engine.convert();
Ok((Graph::new(engine.output_triples), engine.report))
}
pub fn parse_and_upgrade(
input: &str,
format: RdfFormat,
options: UpgradeOptions,
) -> Result<(Graph, UpgradeReport), UpgradeError> {
let graph = Graph::parse(input, format)?;
sbol2_to_sbol3(&graph, options)
}
#[derive(Clone, Copy)]
enum FcDirection {
Input,
Output,
Inout,
}
#[derive(Clone, Copy)]
enum RefinementShape {
UseLocal,
UseRemote,
VerifyIdentical,
}
#[derive(Default, Clone)]
struct MapsToInfo {
local: Option<String>,
remote: Option<String>,
refinement: Option<String>,
display_id: Option<String>,
}
struct Engine<'a> {
input: &'a Graph,
options: UpgradeOptions,
identity: IdentityMap,
typed_subjects: HashMap<String, String>,
cd_sequences: HashMap<String, Vec<String>>,
location_to_sa: HashMap<String, String>,
sa_to_cd: HashMap<String, String>,
sa_to_subcomponent: HashMap<String, String>,
display_ids: HashMap<String, String>,
owned_by: HashMap<String, String>,
mapsto_info: HashMap<String, MapsToInfo>,
fc_directions: HashMap<String, FcDirection>,
fc_direction_none: HashSet<String>,
fc_public_access: HashSet<String>,
output_triples: Vec<Triple>,
namespaced_subjects: HashSet<String>,
used_iris: HashSet<String>,
location_display_id_overrides: HashMap<String, String>,
report: UpgradeReport,
}
fn is_top_level_sbol2(sbol2_type: &str) -> bool {
matches!(
sbol2_type,
v2::SBOL2_COMPONENT_DEFINITION
| v2::SBOL2_MODULE_DEFINITION
| v2::SBOL2_SEQUENCE
| v2::SBOL2_MODEL
| v2::SBOL2_COLLECTION
| v2::SBOL2_IMPLEMENTATION
| v2::SBOL2_ATTACHMENT
| v2::SBOL2_EXPERIMENT
| v2::SBOL2_EXPERIMENTAL_DATA
| v2::SBOL2_COMBINATORIAL_DERIVATION
) || is_prov_top_level(sbol2_type)
}
fn is_prov_top_level(type_iri: &str) -> bool {
matches!(
type_iri,
v3::PROV_ACTIVITY | v3::PROV_AGENT_CLASS | v3::PROV_PLAN
)
}
fn type_precedence(type_iri: &str) -> u8 {
if is_known_sbol2_type(type_iri) || is_prov_top_level(type_iri) {
2
} else if type_iri.starts_with(v2::SBOL2_NS) {
1
} else {
0
}
}
fn is_known_sbol2_type(type_iri: &str) -> bool {
matches!(
type_iri,
v2::SBOL2_COMPONENT_DEFINITION
| v2::SBOL2_MODULE_DEFINITION
| v2::SBOL2_COMPONENT
| v2::SBOL2_MODULE
| v2::SBOL2_FUNCTIONAL_COMPONENT
| v2::SBOL2_SEQUENCE_ANNOTATION
| v2::SBOL2_SEQUENCE_CONSTRAINT
| v2::SBOL2_SEQUENCE
| v2::SBOL2_MODEL
| v2::SBOL2_INTERACTION
| v2::SBOL2_PARTICIPATION
| v2::SBOL2_COLLECTION
| v2::SBOL2_IMPLEMENTATION
| v2::SBOL2_ATTACHMENT
| v2::SBOL2_EXPERIMENT
| v2::SBOL2_EXPERIMENTAL_DATA
| v2::SBOL2_COMBINATORIAL_DERIVATION
| v2::SBOL2_VARIABLE_COMPONENT
| v2::SBOL2_RANGE
| v2::SBOL2_CUT
| v2::SBOL2_GENERIC_LOCATION
| v2::SBOL2_MAPS_TO
)
}
use crate::iri_util::last_iri_segment as last_path_segment;
fn next_available_child_iri(
parent: &str,
base_display_id: &str,
used: &mut HashSet<String>,
) -> (String, String) {
let mut counter: usize = 1;
loop {
let display_id = if counter == 1 {
base_display_id.to_owned()
} else {
format!("{base_display_id}_{counter}")
};
let iri = format!("{parent}/{display_id}");
if used.insert(iri.clone()) {
return (display_id, iri);
}
counter += 1;
}
}
fn next_available_mapsto_iris(
top_level: &str,
base_display_id: &str,
used: &mut HashSet<String>,
) -> (String, String, String, String) {
let mut counter: usize = 1;
loop {
let display_id = if counter == 1 {
base_display_id.to_owned()
} else {
format!("{base_display_id}_{counter}")
};
let cref_iri = format!("{top_level}/{display_id}");
let constraint_display_id = format!("{display_id}_constraint");
let constraint_iri = format!("{top_level}/{constraint_display_id}");
if !used.contains(&cref_iri) && !used.contains(&constraint_iri) {
used.insert(cref_iri.clone());
used.insert(constraint_iri.clone());
return (display_id, cref_iri, constraint_display_id, constraint_iri);
}
counter += 1;
}
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub fn canonical_nt_line(triple: &Triple) -> String {
const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
let subject = match &triple.subject {
Resource::Iri(iri) => format!("<{}>", iri.as_str()),
Resource::BlankNode(b) => format!("_:{}", b.as_str()),
other => unreachable!("unhandled Resource variant: {other:?}"),
};
let predicate = format!("<{}>", triple.predicate.as_str());
let object = match &triple.object {
Term::Resource(Resource::Iri(iri)) => format!("<{}>", iri.as_str()),
Term::Resource(Resource::BlankNode(b)) => format!("_:{}", b.as_str()),
Term::Literal(literal) => {
let escaped = escape_nt_string(literal.value());
if let Some(lang) = literal.language() {
format!("\"{escaped}\"@{lang}")
} else if literal.datatype().as_str() == XSD_STRING {
format!("\"{escaped}\"")
} else {
format!("\"{escaped}\"^^<{}>", literal.datatype().as_str())
}
}
other => unreachable!("unhandled Term variant: {other:?}"),
};
format!("{subject} {predicate} {object} .")
}
fn escape_nt_string(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for c in value.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 || c as u32 == 0x7F => {
out.push_str(&format!("\\u{:04X}", c as u32));
}
_ => out.push(c),
}
}
out
}
fn url_origin(iri: &str) -> Option<String> {
if let Some((scheme, rest)) = iri.split_once("://") {
let authority_end = rest.find('/').unwrap_or(rest.len());
let host = &rest[..authority_end];
if !scheme.is_empty() && !host.is_empty() {
return Some(format!("{scheme}://{host}"));
}
}
if let Some(after_urn) = iri.strip_prefix("urn:")
&& !after_urn.is_empty()
{
let nid_end = after_urn.find([':', '/']).unwrap_or(after_urn.len());
let nid = &after_urn[..nid_end];
if !nid.is_empty() {
return Some(format!("urn:{nid}"));
}
}
None
}