Search code examples
cuelang

Declare two fields of a struct as mutually exclusive in CueLang?


I want to ensure that my users only set one of two fields:

rotations:
  - type: weekly
    time_restrictions:
      # Allow only ONE of the following fields:
      weekday_time_of_day: {...}
      time_of_day: [...]       

I came across the OneOf pattern on Cuetorials, but this does only seem to help when wanting to enforce a schema while writing cue files.


#OneOfTimeRestrictions: {time_of_day: [...string]} | {weekday_time_of_day: [...string]}

rotations: [{
    type:         *"weekly" | "daily"
    restrictions: #OneOfTimeRestrictions | {} // won't work, naturally, because nothing is "chosen" 
}]

(The values of the mutually exclusive fields are actually additional, more complex structs, not strings, in case that might matter - but for the sake of a shorter example I've omitted them.)

However, I'm trying to vet YAML instead.

The problem is that when defining this:

#OneOfTimeRestrictions: rotations: [{
    type:         *"weekly" | "daily"
    restrictions: {time_of_day: [...string]} | {weekday_time_of_day: [...string]}
}]

Both fields are acceptable, including when giving them at the same time.

Pointers?


Solution

  • Not a cue expert but,
    in CUE {} can be a closed or an open structure depending on the context.
    #A: {} is a closed schema, meaning you can't add new field to it.
    While B:{} is an open value where you can add new field to it.

    #A:{}
    a: a & {someKey: true} // will produce an error
    
    B:{}
    b: b & {someKey: true} // will pass
    
    _C:{}
    c: _C & {someKey: true} // will pass
    

    (You can test it here: https://cuelang.org/play/?id=XigxaAJ1bcp#cue@export@cue)
    (And read more about it here: https://cuetorials.com/deep-dives/closedness/)

    So in your first CUE code example, the line restrictions: #OneOfTimeRestrictions | {} says restrictions must match either a closed schema with either a weekday_time_of_day or time_of_day field and nothing else or must match an unclosed {} which will match every non-null object.

    In your second code example, you are saying restictions as to match to an open structure that has the weekday_time_of_day field or match to an open structure that has the time_of_day field. So, they are basically identical.

    Try it like this

    #OneOfTimeRestrictions: {time_of_day: [...string]} | {weekday_time_of_day: [...string]}
    
    
    #Restrictions: {
        restrictionA: string
        restrictionB: bool
        // ...
        restrictionZ: bool
    
        #OneOfTimeRestrictions
    }
    
    rotations: [...{
        type: *"weekly" | "daily"
        restrictions: #Restrictions
    }]
    

    Or if you don't like the additional schema, like this

    #OneOfTimeRestrictions: {time_of_day: [...string]} | {weekday_time_of_day: [...string]}
    
    rotations: [...{
        type: *"weekly" | "daily"
        restrictions: {
            restrictionA:     string
            restrictionB:     bool
            // ...
            restrictionZ: bool
            
            #OneOfTimeRestrictions
        }
    }]
    

    But these solutions will make the objects in relations closed. So, you will have deliberately defined every additional restriction.
    (I'm not certain why restrictions is close in my second example, but in my test it is closed).

    If you need that the objects inside relations are open, you can use something like this:

    rotations: [...{
        type: *"weekly" | "daily"
        restrictions: {
    
            time_of_day: [...string]
            weekday_time_of_day: [...string]
    
            _tr1: bool | *false
            _tr2: bool | *false
    
            if time_of_day == [] {
                _tr1: true
            }
            if weekday_time_of_day == [] {
                _tr2: true
            }
    
            _time_restiction_valid: true
            _time_restiction_valid: (_tr1 && !_tr2) || (!_tr1 && _tr2) // append '|| (!_tr1 && !_tr2)' if you want to allow that nither can be set
        }
    }]