owl2shacl 0.2.0

A CLI tool that tries to convert simple OWL ontologies into SHACL shapes. OWL ontologies define logical relationships. SHACL shapes define a data scheme, and allow to validate data against them. Strictly speaking, as these are different things, such a conversion is thus illegal/wrong in the ideological/theoretical sense. Thus this tool is not generally applicable, but only under the circumstance, that the OWL ontology is actually written as a data specification - if it is understood as a kind of distributed database schema, rather then for logical inference. Not only that, but it also has to conform to certain, very narrow rules, and only a few basic properties are translated into SHACL; the rest is ignored.
// SPDX-FileCopyrightText: 2024 Robin Vobruba <hoijui.quaero@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use std::path::{Path, PathBuf};

use clap::ValueEnum;
use enum_map::{Enum, EnumMap};
use strum_macros::{EnumIter, EnumString, IntoStaticStr, VariantNames};

/**
 * How to behave in case an oddity is detected
 * in the source Ontology.
 */
#[derive(
    Debug,
    ValueEnum,
    EnumString,
    VariantNames,
    EnumIter,
    IntoStaticStr,
    PartialEq,
    Eq,
    PartialOrd,
    Copy,
    Clone,
)]
pub enum OddityHandling {
    Ignore,
    Warn,
    Error,
}

impl Default for OddityHandling {
    fn default() -> Self {
        Self::Warn
    }
}

impl OddityHandling {
    pub const fn ignore(self) -> bool {
        matches!(self, Self::Ignore)
    }
}

#[derive(Clone, Copy, Debug, Enum, EnumIter)]
pub enum RDProperty {
    Range,
    Domain,
}

impl RDProperty {
    pub const fn to_str(self) -> &'static str {
        match self {
            Self::Range => "range",
            Self::Domain => "domain",
        }
    }
}

#[derive(Clone, Debug, Default)]
pub struct Settings /*<S: ::std::hash::BuildHasher>*/ {
    /**
     * What to do if the source Ontology contains properties
     * with `rdfs:range`/`rdfs:domain` that specifies a list/array of classes,
     * which means that possible objects have to implement
     * _all_ of these classes,
     * which is often not what was intended.
     */
    pub and_list_detected: EnumMap<RDProperty, OddityHandling>,
    /**
     * What to do if the source Ontology contains properties
     * using both `rdfs:range` and `*:rangeIncludes`,
     * or respectively `rdfs:domain` and `*:domainIncludes`,
     * which is somewhat ill-defined.
     */
    pub style_mix_property: EnumMap<RDProperty, OddityHandling>,
    /**
     * What to do if the source Ontology contains both properties
     * using `rdfs:range` and others using `*:rangeIncludes`,
     * or respectively `rdfs:domain` and others using `*:domainIncludes`,
     * which is technically ok, but might be confusing.
     */
    pub style_mix_ontology: EnumMap<RDProperty, OddityHandling>,
}

#[derive(Clone, Debug)]
pub struct Config {
    pub settings: Settings,
    pub base_iri: Option<String>,
    pub owl: PathBuf,
    pub shacl: Option<PathBuf>,
    pub prefixes: Option<PathBuf>,
}

impl Config {
    fn insert_postfix(orig: Option<PathBuf>, base: &Path, postfix: &str) -> PathBuf {
        orig.unwrap_or_else(|| {
            let file_name_orig = base.file_name().unwrap();
            let file_name_new = file_name_orig
                .to_string_lossy()
                .as_ref()
                .replace(".ttl", &format!("-{postfix}.ttl"));
            base.with_file_name(file_name_new)
        })
    }

    pub fn shacl(&self) -> PathBuf {
        Self::insert_postfix(self.shacl.clone(), &self.owl, "shacl")
    }

    pub fn prefixes(&self) -> PathBuf {
        Self::insert_postfix(
            self.prefixes.clone(),
            self.shacl.as_ref().unwrap_or(&self.owl),
            "prefixes",
        )
    }
}