cue-lang / cue

The home of the CUE language! Validate and define text-based and dynamic configuration
https://cuelang.org
Apache License 2.0
5.12k stars 292 forks source link

evaluator: encoding of JSON Schema (and other) oneofs #3165

Open myitcv opened 5 months ago

myitcv commented 5 months ago

What version of CUE are you using (cue version)?

$ cue version
cue version v0.0.0-20240522083645-3ce48c0beadf

go version go1.22.3
      -buildmode exe
       -compiler gc
  DefaultGODEBUG httplaxcontentlength=1,httpmuxgo121=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1
     CGO_ENABLED 1
          GOARCH arm64
            GOOS linux
             vcs git
    vcs.revision 3ce48c0beadfc8b3d22285a197c9ebd53f4bd59a
        vcs.time 2024-05-22T08:36:45Z
    vcs.modified false
cue.lang.version v0.9.0

Does this issue reproduce with the latest release?

Yes

What did you do?

Capturing an issue that deals with how to encode one-ofs from JSON Schema, protocol buffers (and potentially other languages). #943 suggests one approach for encoding oneofs; this issue is a more general exploration of the space.

The example in question is from JSON Schema:

{
  "oneOf": [
    {
      "required": [
        "x"
      ]
    },
    {
      "required": [
        "y"
      ]
    }
  ]
}

As can be seen from https://jsonschema.dev/s/5KO7a, this validates successfully when one field is present, and raises an error when both are specified https://jsonschema.dev/s/209Bo.

Turning to how this can be represented in CUE we have, AFAICT, two ways of doing this today:

// approach 1
_constraint: {x!: int} | {y!: int}
// approach 2
_constraint: {x!: int, y?: _|_} | {y!: int, x?: _|_}

Looking at how each behaves with both the old and new evaluator we see the following:

# Old evaluator - approach 1
env CUE_EXPERIMENT=''
! exec cue export x.cue approach1.cue
cmp stderr old_1_stderr.golden

# Old evaluator - approach 2
env CUE_EXPERIMENT=''
exec cue export x.cue approach2.cue
cmp stdout old_2_stdout.golden

# New evaluator - approach 1
env CUE_EXPERIMENT='evalv3'
! exec cue export x.cue approach1.cue
cmp stderr new_1_stderr.golden

# Old evaluator - approach 2
env CUE_EXPERIMENT='evalv3'
exec cue export x.cue approach2.cue
cmp stdout new_2_stdout.golden

-- approach1.cue --
package x

_constraint: {x!: int} | {y!: int}
-- approach2.cue --
package x

_constraint: {x!: int, y?: _|_} | {y!: int, x?: _|_}
-- x.cue --
package x

res: _constraint & {
    x: 5
}
-- old_1_stderr.golden --
res: incomplete value {x:5} | {x:5,y!:int}
-- new_1_stderr.golden --
res: incomplete value {x:5} | {x:5,y!:int}
-- old_2_stdout.golden --
{
    "res": {
        "x": 5
    }
}
-- new_2_stdout.golden --
{
    "res": {
        "x": 5
    }
}

What did you expect to see?

Unclear.

Export of approach 2 succeeds, but I don't think it is a scaleable approach to encoding oneofs. It is also not readily understandable by humans. It's not incorrect however that the cue export succeeds in this case.

Export of approach 1 fails. Given my understanding of when the required field is "checked", I think this therefore qualifies as "behaving as expected". But I'm not clear that this is the behaviour we want. I think it's reasonable to make the argument that cue export explicitly/implicitly says "I am not going to provide you with anything more". Through that lens, one could argue that the {y!: int} disjunct in approach 1 can be eliminated, because it fails with respect to {x: 5}, which would then allow the export to succeed.

Hence there's an argument that the test should fail, because (if you will excuse the double negative) approach 1 should succeed and give the same output as approach 2.

What did you see instead?

Passing test.

Related issues

In raising this issue I suggest we consolidate conversation from the following issues:

Update: those issues have now been closed to consolidate discussion here.

myitcv commented 3 months ago

@rogpeppe also flags this canonical examples from JSON Schema: https://json-schema.org/understanding-json-schema/reference/combining#oneOf

{
  "oneOf": [
    { "type": "number", "multipleOf": 5 },
    { "type": "number", "multipleOf": 3 }
  ]
}
chiragjn commented 2 months ago

Adding one more example We are facing troubles with correct code generation because oneOf has allOf + not anyOf

import "strings"

#SpecialText: {
  type: "special",
  text: string & =~"^.{1,}$"
}

#MyText: {
  content: strings.MinRunes(1) | #SpecialText
}
"components": {
        "schemas": {
            "MyText": {
                "type": "object",
                "required": [
                    "content"
                ],
                "properties": {
                    "content": {
                        "oneOf": [
                            {
                                "allOf": [
                                    {
                                        "type": "string",
                                        "minLength": 1
                                    },
                                    {
                                        "not": {
                                            "anyOf": [
                                                {
                                                    "$ref": "#/components/schemas/SpecialText"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            },
                            {
                                "$ref": "#/components/schemas/SpecialText"
                            }
                        ]
                    }
                }
            },
            "SpecialText": {
                "type": "object",
                "required": [
                    "type",
                    "text"
                ],
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "special"
                        ]
                    },
                    "text": {
                        "type": "string",
                        "pattern": "^.{1,}$"
                    }
                }
            }
        }
    }
myitcv commented 2 months ago

I've just raised https://github.com/cue-lang/cue/issues/3380 to track the implementation of oneof and related constraints using the new matchN builtin.