Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spec: allow record {} <: record {null} #462

Merged
merged 4 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions rust/candid/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ impl<'de> IDLDeserialize<'de> {
if self.de.types.is_empty() {
if matches!(
expected_type.as_ref(),
TypeInner::Opt(_) | TypeInner::Reserved
TypeInner::Opt(_) | TypeInner::Reserved | TypeInner::Null
) {
self.de.expect_type = expected_type;
self.de.wire_type = TypeInner::Reserved.into();
return T::deserialize(&mut self.de);
} else {
return Err(Error::msg(format!(
"No more values on the wire, the expected type {expected_type} is not opt or reserved"
"No more values on the wire, the expected type {expected_type} is not opt, reserved or null"
chenyan-dfinity marked this conversation as resolved.
Show resolved Hide resolved
)));
}
}
Expand Down Expand Up @@ -548,7 +548,8 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
{
self.unroll_type()?;
check!(
*self.expect_type == TypeInner::Null && *self.wire_type == TypeInner::Null,
*self.expect_type == TypeInner::Null
&& matches!(*self.wire_type, TypeInner::Null | TypeInner::Reserved),
chenyan-dfinity marked this conversation as resolved.
Show resolved Hide resolved
"unit"
);
visitor.visit_unit()
Expand Down
7 changes: 5 additions & 2 deletions rust/candid/tests/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,12 +697,15 @@ fn test_multiargs() {
Vec<(Int, &str)>,
(Int, String),
Option<i32>,
candid::Reserved,
(),
candid::Reserved
)
.unwrap();
assert_eq!(tuple.2, None);
assert_eq!(tuple.3, candid::Reserved);
#[allow(clippy::unit_cmp)]
{
assert_eq!(tuple.3, ());
}
assert_eq!(tuple.4, candid::Reserved);
}

Expand Down
10 changes: 5 additions & 5 deletions spec/Candid.md
Original file line number Diff line number Diff line change
Expand Up @@ -832,11 +832,11 @@ record { <fieldtype>;* } <: record { <fieldtype'>;* }
record { <nat> : <datatype>; <fieldtype>;* } <: record { <nat> : <datatype'>; <fieldtype'>;* }
```

In order to be able to evolve and extend record types that also occur in inbound position (i.e., are used both as function results and function parameters), the subtype relation also supports *removing* fields from records, provided they are optional (or `reserved`).
In order to be able to evolve and extend record types that also occur in inbound position (i.e., are used both as function results and function parameters), the subtype relation also supports *removing* fields from records, provided they are optional, null, or `reserved`.
```
<nat> not in <fieldtype>;*
record { <fieldtype>;* } <: record { <fieldtype'>;* }
opt empty <: <datatype'>
null <: <datatype'>
------------------------------------------------------------------------------
record { <fieldtype>;* } <: record { <nat> : <datatype'>; <fieldtype'>;* }
```
Expand Down Expand Up @@ -980,11 +980,11 @@ In the following rule:

* The `<nat1>*` field names are those present in both the actual and expected type. The values are coerced.
* the `<nat2>*` field names those only in the actual type. The values are ignored.
* the `<nat3>*` field names are those only in the expected type, which therefore must be of optional or reserved type. The `null` value is used for these.
* the `<nat3>*` field names are those only in the expected type, which therefore must be of null, optional or reserved type. The `null` value is used for these.

```
<v1> : <t1> ~> <v1'> : <t1'>
opt empty <: <t3>
null <: <t3>
-------------------------------------------------------------------------------------------
record { <nat1> = <v1>;* <nat2> = <v2>;* } : record { <nat1> : <t1>;* <nat2> : <t2>;* } ~>
record { <nat1> = <v1'>;* <nat3> = null;* } : record { <nat1> : <t1'>;* <nat3> : <t3>;* }
Expand Down Expand Up @@ -1023,7 +1023,7 @@ NOTE: These rules above imply that a Candid decoder has to be able to decide the

#### Tuple types

Whole argument and result sequences are coerced with the same rules as tuple-like records. In particular, extra arguments are ignored, and optional parameters read as as `null` if the argument is missing or fails to coerce:
Whole argument and result sequences are coerced with the same rules as tuple-like records. In particular, extra arguments are ignored, and optional/null parameters read as `null` if the argument is missing or fails to coerce:

```
record { <v>;* } : record { <t>;* } ~> record { <v'>;* } : record { <t'>;* }
Expand Down
3 changes: 2 additions & 1 deletion test/construct.test.did
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ assert blob "DIDL\01\6c\01\01\7c\01\00\2a" == "(record {})"
assert "(record { whatever = 0 })" == "(record {})" : (record {}) "record: ignore fields (textual)";
assert blob "DIDL\01\6c\01\01\7c\01\00\2a" !: (record {2:int}) "record: missing field";
assert blob "DIDL\01\6c\01\01\7c\01\00\2a" == "(record { 2 = null })" : (record {2:opt int}) "record: missing opt field";
assert blob "DIDL\01\6c\00\01\00" == "(record { 2 = null })" : (record {2:null}) "record: missing null field";
assert blob "DIDL\01\6c\02\00\7c\01\7e\01\00\2a\01" == "(record {42; true})" : (record {int; bool}) "record: tuple";
assert blob "DIDL\01\6c\02\00\7c\01\7e\01\00\2a\01" == "(record {1 = true})" : (record {1:bool}) "record: ignore fields";
assert blob "DIDL\01\6c\02\00\7c\01\7e\01\00\2a\01" !: (record {bool; int}) "record: type mismatch";
Expand Down Expand Up @@ -193,7 +194,7 @@ assert blob "DIDL\02\6b\02\d1\a7\cf\02\7f\f1\f3\92\8e\04\01\6c\02\a0\d2\ac\a8\04
== "(variant { cons = record { head = 1; tail = variant { cons = record { head = 2; tail = variant { nil } } } } })" : (VariantList) "variant: list";

assert blob "DIDL\02\6b\02\d1\a7\cf\02\7f\f1\f3\92\8e\04\01\6c\02\a0\d2\ac\a8\04\7c\90\ed\da\e7\04\00\01\00\00"
== "(variant {nil}, null, null, null, null)" : (VariantList, opt VariantList, opt empty, reserved, opt int) "variant: extra args";
== "(variant {nil}, null, null, null, null)" : (VariantList, opt VariantList, null, reserved, opt int) "variant: extra args";

assert blob "DIDL\02\6b\02\d1\a7\cf\02\7f\f1\f3\92\8e\04\01\6c\02\a0\d2\ac\a8\04\7c\90\ed\da\e7\04\00\01\00\00" !: (VariantList, opt int, vec int) "non-null extra args";

Expand Down
3 changes: 2 additions & 1 deletion test/prim.test.did
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ assert blob "DIDL\00\01\5e" !: () "Out of range type";
// Missing arguments
assert blob "DIDL\00\00" !: (nat) "missing argument: nat fails";
assert blob "DIDL\00\00" !: (empty) "missing argument: empty fails";
assert blob "DIDL\00\00" !: (null) "missing argument: null fails";
assert blob "DIDL\00\00" == "(null)" : (null) "missing argument: null";
assert blob "DIDL\00\00" == "(null)" : (opt empty) "missing argument: opt empty";
assert blob "DIDL\00\00" == "(null)" : (opt null) "missing argument: opt null";
assert blob "DIDL\00\00" == "(null)" : (opt nat) "missing argument: opt nat";
Expand Down Expand Up @@ -189,6 +189,7 @@ assert blob "DIDL\00\01\70" == blob "DIDL\00\01\7f" : (reserved) "reser
assert blob "DIDL\00\01\70" == blob "DIDL\00\01\7e\01" : (reserved) "reserved from bool";
assert blob "DIDL\00\01\70" == blob "DIDL\00\01\7d\80\01" : (reserved) "reserved from nat";
assert blob "DIDL\00\01\70" == blob "DIDL\00\01\71\06Motoko" : (reserved) "reserved from text";
assert blob "DIDL\00\00" : (reserved) "reserved extra value";
assert blob "DIDL\00\01\71\05Motoko" !: (reserved) "reserved from too short text";
assert blob "DIDL\00\01\71\03\e2\28\a1" !: (reserved) "reserved from invalid utf8 text";

Expand Down
Loading