| 2045 | ) |
| 2046 | @mapcls_and_from_attributes |
| 2047 | def test_struct_union(self, tag1, tag2, unknown, mapcls, from_attributes): |
| 2048 | def decode(msg): |
| 2049 | return convert( |
| 2050 | mapcls(msg), Union[Test1, Test2], from_attributes=from_attributes |
| 2051 | ) |
| 2052 | |
| 2053 | class Test1(Struct, tag=tag1): |
| 2054 | a: int |
| 2055 | b: int |
| 2056 | c: int = 0 |
| 2057 | |
| 2058 | class Test2(Struct, tag=tag2): |
| 2059 | x: int |
| 2060 | y: int |
| 2061 | |
| 2062 | # Tag can be in any position |
| 2063 | assert decode({"type": tag1, "a": 1, "b": 2}) == Test1(1, 2) |
| 2064 | assert decode({"a": 1, "type": tag1, "b": 2}) == Test1(1, 2) |
| 2065 | assert decode({"x": 1, "y": 2, "type": tag2}) == Test2(1, 2) |
| 2066 | |
| 2067 | # Optional fields still work |
| 2068 | assert decode({"type": tag1, "a": 1, "b": 2, "c": 3}) == Test1(1, 2, 3) |
| 2069 | assert decode({"a": 1, "b": 2, "c": 3, "type": tag1}) == Test1(1, 2, 3) |
| 2070 | |
| 2071 | # Extra fields still ignored |
| 2072 | assert decode({"a": 1, "b": 2, "d": 4, "type": tag1}) == Test1(1, 2) |
| 2073 | |
| 2074 | # Tag missing |
| 2075 | with pytest.raises(ValidationError) as rec: |
| 2076 | decode({"a": 1, "b": 2}) |
| 2077 | assert "missing required field `type`" in str(rec.value) |
| 2078 | |
| 2079 | # Tag wrong type |
| 2080 | with pytest.raises(ValidationError) as rec: |
| 2081 | decode({"type": 123.456, "a": 1, "b": 2}) |
| 2082 | assert f"Expected `{type(tag1).__name__}`" in str(rec.value) |
| 2083 | assert "`$.type`" in str(rec.value) |
| 2084 | |
| 2085 | # Tag unknown |
| 2086 | with pytest.raises(ValidationError) as rec: |
| 2087 | decode({"type": unknown, "a": 1, "b": 2}) |
| 2088 | assert f"Invalid value {unknown!r} - at `$.type`" == str(rec.value) |
| 2089 | |
| 2090 | @pytest.mark.parametrize( |
| 2091 | "tag1, tag2, tag3, unknown", |