Re: SHACL Shape definition for including and excluding parent types

Hi Scott,

I think you're over thinking this. From what I can tell, you have a simple requirement.

Firstly, an obvious error: You have your sh:not constraint in your list of sh:or options.
So you're effectively saying "class is P_cls1 OR P_cls2 OR P_cls3 OR NOT ex:P_clsInvalid_a" which as you can see is not logically consistent, because it would pass for any​ parent class that is NOT ex:P_clsInvalid_a.
You instead need an AND in there, like "(class is P_cls1 OR P_cls2 OR P_cls3) AND (NOT ex:P_clsInvalid_a)" like this:
sh:and (
  [
    sh:or (
      [ sh:class  ex:P_cls1 ]
      [ sh:class  ex:P_cls2 ]
      [ sh:class  ex:P_cls3 ]
    )
  ]
  [ sh:not [ sh:class ex:P_clsInvalid_a ] ]
)

Secondly, to match one and only one of those in the OR list, use exclusive-or logic (sh:xone in SHACL<https://www.w3.org/TR/shacl/#XoneConstraintComponent>). Like this:
sh:xone (
  [ sh:class  ex:P_cls1 ]
  [ sh:class  ex:P_cls2 ]
  [ sh:class  ex:P_cls3 ]
) ;

Next, if you want to maintain a list of invalid options, you can use sh:in<https://www.w3.org/TR/shacl/#InConstraintComponent> wrapped in a sh:not, to make a "not in" constraint. Like this:
sh:not [ sh:in ( ex:P_clsInvalid_a ex:P_clsInvalid_b ) ] ;

But when you break it down that far, why do you need the NOT? Wouldn't just "(class is P_cls1 XOR P_cls2 XOR P_cls3)" do the trick? Anything with a parent not in that list will be invalid, why does there need to be invalid classes specified, unless those invalid class are subclasses of P_cls1, P_cls2, P_cls3 ?
Or are there other unspecified parent classes which are not in the list of three possible required, but also not invalid? If that is the case, your requirements need to be restated like:

  *   path ex:parent will have only one value
  *   the value for ex:parent can have one or more types (classes)
  *   one of those types needs to be {P_cls1 OR P_cls2 OR P_cls3}
  *   maximum of one of those types can be {P_cls1, P_cls2, P_cls3}
  *   parent's types cannot be one of {ex:P_clsInvalid_a, ex:P_clsInvalid_b}

You can use qualifiedvalueshape to ensure that at least one of the parents match the OR constraint, not need to use sh:xone in this case.
Note, to emulate sh:class semantics on value-taking shapes like qualifiedValueShape and sh:in, I am using the same transitive SHACL Properly Path as demonstrated in the sh:class docs.<https://www.w3.org/TR/shacl/#ClassConstraintComponent>

So, if I were writing a shape to match the requirements above, it would look like this:

my_sh:ParentTypeRestriction
  a sh:NodeShape ;
  sh:targetClass ex:Cls1 ;
  sh:property [
    sh:path ex:parent ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:property [
      sh:path ( rdf:type [ sh:zeroOrMorePath rdfs:subClassOf ] ) ;
      sh:minCount 1 ;
      sh:qualifiedMinCount 1 ;
      sh:qualifiedMaxCount 1 ;
      sh:qualifiedValueShape [
        sh:or (
          [ sh:hasValue ex:P_cls1 ]
          [ sh:hasValue ex:P_cls2 ]
          [ sh:hasValue ex:P_cls3 ]
        )
      ] ;
      sh:not [ sh:in ( ex:P_clsInvalid_a ex:P_clsInvalid_b ) ] ;
    ] ;
    sh:message "parent type for ex:Cls1 is not in x:P_cls{1, 2, 3}" ;
  ] ;
.
(Tested in PySHACL, using the valid and invalid examples you gave in your correspondence).

This may be overkill for your requirements, and I may have misinterpreted your use-case. But it should at least give you something to work with.
It could be taken even further by doing regex on type names to match P_cls{1,2,3}, if there is a known pattern they match, but that might be going too far.

- Ashley

________________________________
From: Scott Henninger <scotthenninger@gmail.com>
Sent: Tuesday, 14 September 2021 3:24 PM
To: public-shacl@w3.org <public-shacl@w3.org>
Subject: SHACL Shape definition for including and excluding parent types

I have a couple of scenarios that involve restrictions on classes.  Most of them I can get working, but a couple I'm finding a bit difficult to figure out.  To make it simple, I'll start with the parent type restriction I'm trying to express.

The following would be a valid shape as it targets ex:Cls1, specifies a parent with ex:parent, and the parent has one of the types ex:P_cls{1, 2, 3}.  (In all of these cases the targetClass is ex:Cls1).
   ex:Child1
      a ex:Cls1 ;
      ex:parent ex:Parent1 ;
.
   ex:Parent1
      a ex:Pcls1 ;
.

This would be an invalid shape because ex:Pcls{1,2,3} types must be defined for the parent:
   ex:Child1
      a ex:Cls1 ;
      ex:parent ex:Parent1 ;
.
   ex:Parent1
      a ex:P_clsInvalid_a ;
.

This one is valid because it includes one of ex:Pcls{1,2,3}:
   ex:Child1
      a ex:Cls1 ;
      ex:parent ex:Parent1 ;
   .
   ex:Parent1
      a ex:P_cls2, ex:P_clsInvalid_a ;
   .

The fourth is invalid because only one of ex:Pcls{1, 2, 3} is allowed:
   ex:Child1
      a ex:Cls1 ;
      ex:parent ex:Parent1 ;
.
   ex:Parent1
      a ex:P_cls1, ex:P_cls2;
.

If I could live with a list of disallowed types (for the sake of maintenance I'd rather say allow only ex:P_cls{1, 2, 3}) then the following seems to work:
   my_sh:ParentTypeRestriction
         a sh:NodeShape ;
        sh:targetClass ex:Cls1 ;
        sh:property[
         s h:path ex:parent ;
         sh:or (
            [ sh:class  ex:P_cls1 ]
            [ sh:class  ex:P_cls2 ]
            [ sh:class  ex:P_cls3 ]
            [ sh:not [ sh:class ex:P_clsInvalid_a ] ]
        ) ;
     sh:message "parent type for ex:Cls1 is not in x:P_cls{1, 2, 3}" ;
] ;
.

..where I include an exhaustive list for sh:not.  As stated before, this isn't the best when the model changes and I need to find all the places where it needs to be excluded.

So any ideas on how to 1) improve this shape, or 2) future-proof it for future additions to the rsf:type list?

Thanks a bunch for taking a look.
-- Scott

Scott Henninger
scotthenninger@gmail.com<mailto:scotthenninger@gmail.com>

Received on Friday, 17 September 2021 08:02:46 UTC