@prefix : <https://ontology.inferal.com/modules/sets/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:SetShape
    a sh:NodeShape ;
    rdfs:label "set shape" ;
    rdfs:comment "Validates set membership occurrences, direct member assertions, duplicate member exclusion, and asserted cardinality." ;
    sh:targetClass :Set ;
    sh:property [
        sh:path :hasMembership ;
        sh:class :Membership ;
    ] ;
    sh:property [
        sh:path :hasMember ;
        sh:nodeKind sh:BlankNodeOrIRI ;
    ] ;
    sh:property [
        sh:path :cardinality ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "A classical Set must not have duplicate membership occurrences for the same member resource." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/sets/>
            SELECT $this ?member ?left ?right WHERE {
                ?left :inSet $this ;
                      :member ?member .
                ?right :inSet $this ;
                       :member ?member .
                FILTER(?left != ?right)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A direct hasMember assertion must be backed by a membership occurrence for the same set and member." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/sets/>
            SELECT $this ?member WHERE {
                $this :hasMember ?member .
                FILTER NOT EXISTS {
                    ?membership :inSet $this ;
                                :member ?member .
                }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A hasMembership link must point to a membership occurrence whose inSet is the same set." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/sets/>
            SELECT $this ?membership ?declaredSet WHERE {
                $this :hasMembership ?membership .
                OPTIONAL { ?membership :inSet ?declaredSet . }
                FILTER(!BOUND(?declaredSet) || ?declaredSet != $this)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "When cardinality is asserted, it must equal the number of explicit membership occurrences." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/sets/>
            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
            SELECT $this ?cardinality ?actual WHERE {
                $this :cardinality ?cardinality .
                {
                    SELECT $this (COUNT(?membership) AS ?actual) WHERE {
                        OPTIONAL { ?membership :inSet $this . }
                    }
                    GROUP BY $this
                }
                FILTER(xsd:integer(?cardinality) != ?actual)
            }
        """ ;
    ] .

:MembershipShape
    a sh:NodeShape ;
    rdfs:label "membership shape" ;
    rdfs:comment "Validates that membership occurrences identify exactly one set and exactly one member resource." ;
    sh:targetClass :Membership ;
    sh:property [
        sh:path :inSet ;
        sh:class :Set ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :member ;
        sh:nodeKind sh:BlankNodeOrIRI ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] .
