@prefix : <https://ontology.inferal.com/modules/json-values/> .
@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#> .

:ValueShape
    a sh:NodeShape ;
    rdfs:label "value shape" ;
    rdfs:comment "Validates JSON value resources as exactly one JSON object, array, string, number, boolean, or null value and rejects recursive containment cycles." ;
    sh:targetClass :Value ;
    sh:targetClass :Object ;
    sh:targetClass :Array ;
    sh:targetClass :String ;
    sh:targetClass :Number ;
    sh:targetClass :Boolean ;
    sh:targetClass :Null ;
    sh:xone (
        [ sh:class :Object ]
        [ sh:class :Array ]
        [ sh:class :String ]
        [ sh:class :Number ]
        [ sh:class :Boolean ]
        [ sh:class :Null ]
    ) ;
    sh:sparql [
        sh:message "JSON value graphs must be acyclic through object member values and array element values." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this WHERE {
                $this (:hasMember/:memberValue|:hasElement/:elementValue)+ $this .
            }
        """ ;
    ] .

:ObjectShape
    a sh:NodeShape ;
    rdfs:label "object shape" ;
    rdfs:comment "Validates JSON object members, reverse object membership links, and member index consistency." ;
    sh:targetClass :Object ;
    sh:property [
        sh:path :hasMember ;
        sh:class :Member ;
    ] ;
    sh:sparql [
        sh:message "A hasMember link must point to a member whose memberOfObject is the same JSON object." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?member ?declaredObject WHERE {
                $this :hasMember ?member .
                OPTIONAL { ?member :memberOfObject ?declaredObject . }
                FILTER(!BOUND(?declaredObject) || ?declaredObject != $this)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "Object memberIndex values must be unique within one JSON object." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?index WHERE {
                ?left :memberOfObject $this ;
                    :memberIndex ?index .
                ?right :memberOfObject $this ;
                    :memberIndex ?index .
                FILTER(?left != ?right)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "Object memberIndex values must be all-or-none within one JSON object." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?member WHERE {
                ?indexed :memberOfObject $this ;
                    :memberIndex ?index .
                ?member :memberOfObject $this .
                FILTER NOT EXISTS { ?member :memberIndex ?memberIndex . }
            }
        """ ;
    ] .

:ArrayShape
    a sh:NodeShape ;
    rdfs:label "array shape" ;
    rdfs:comment "Validates JSON array elements, reverse array membership links, and contiguous element indexes." ;
    sh:targetClass :Array ;
    sh:property [
        sh:path :hasElement ;
        sh:class :Element ;
    ] ;
    sh:sparql [
        sh:message "A hasElement link must point to an element whose elementOfArray is the same JSON array." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?element ?declaredArray WHERE {
                $this :hasElement ?element .
                OPTIONAL { ?element :elementOfArray ?declaredArray . }
                FILTER(!BOUND(?declaredArray) || ?declaredArray != $this)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "Array elementIndex values must be unique within one JSON array." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?index WHERE {
                ?left :elementOfArray $this ;
                    :elementIndex ?index .
                ?right :elementOfArray $this ;
                    :elementIndex ?index .
                FILTER(?left != ?right)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "Array elementIndex values must be contiguous from zero to length minus one." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this WHERE {
                {
                    SELECT $this (COUNT(DISTINCT ?element) AS ?count) (MAX(?index) AS ?maxIndex) WHERE {
                        ?element :elementOfArray $this ;
                            :elementIndex ?index .
                    }
                    GROUP BY $this
                }
                FILTER(?count > 0 && ?maxIndex != (?count - 1))
            }
        """ ;
    ] .

:MemberShape
    a sh:NodeShape ;
    rdfs:label "member shape" ;
    rdfs:comment "Validates JSON object member ownership, name, value, optional index, and matching forward hasMember links." ;
    sh:targetClass :Member ;
    sh:property [
        sh:path :memberOfObject ;
        sh:class :Object ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :memberName ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :memberValue ;
        sh:class :Value ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :memberIndex ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "A memberOfObject assertion must be backed by a forward hasMember assertion." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?object WHERE {
                $this :memberOfObject ?object .
                FILTER NOT EXISTS { ?object :hasMember $this . }
            }
        """ ;
    ] .

:ElementShape
    a sh:NodeShape ;
    rdfs:label "element shape" ;
    rdfs:comment "Validates JSON array element ownership, index, value, and matching forward hasElement links." ;
    sh:targetClass :Element ;
    sh:property [
        sh:path :elementOfArray ;
        sh:class :Array ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :elementIndex ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :elementValue ;
        sh:class :Value ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "An elementOfArray assertion must be backed by a forward hasElement assertion." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this ?array WHERE {
                $this :elementOfArray ?array .
                FILTER NOT EXISTS { ?array :hasElement $this . }
            }
        """ ;
    ] .

:StringShape
    a sh:NodeShape ;
    rdfs:label "string shape" ;
    rdfs:comment "Validates the required lexical string value of JSON string resources." ;
    sh:targetClass :String ;
    sh:property [
        sh:path :stringValue ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] .

:NumberShape
    a sh:NodeShape ;
    rdfs:label "number shape" ;
    rdfs:comment "Validates the required JSON number lexical form for JSON number resources." ;
    sh:targetClass :Number ;
    sh:property [
        sh:path :numberLexicalForm ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:pattern "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$" ;
    ] .

:BooleanShape
    a sh:NodeShape ;
    rdfs:label "boolean shape" ;
    rdfs:comment "Validates the required boolean value of JSON boolean resources." ;
    sh:targetClass :Boolean ;
    sh:property [
        sh:path :booleanValue ;
        sh:datatype xsd:boolean ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] .

:NullShape
    a sh:NodeShape ;
    rdfs:label "null shape" ;
    rdfs:comment "Validates that JSON null is represented by the singleton json:null individual." ;
    sh:targetClass :Null ;
    sh:sparql [
        sh:message "The JSON null value must be represented by the singleton json:null individual." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/json-values/>
            SELECT $this WHERE {
                FILTER($this != :null)
            }
        """ ;
    ] .
