🚧 Rust protocol schema testing 🚧
Rust protocol schema testing
How to test if the change made to the protocol schemes (
oxidation/libparsec/crates/{protocol,client_types}/schema/**.json) is valid ?
- Testing retro-compatibility.
- Ensure that major versions are indeed not compatible together (i.e.:
v1againstv2). - Ensure that minor versions are compatible to each other (i.e.:
v1.1withv1.3).
- Ensure that major versions are indeed not compatible together (i.e.:
- Testing if we aren't creating multiple unreleased version.
Multiple unreleased version MUST be squashed together
- Testing if we aren't editing an already released protocol version.
TODOS
- [ ] #3074
- [ ] #3075
- [ ] Add checker for previous python scheme against rust one
- [ ] Add checker for
MAJOR-ENEMIES - [ ] Add checker for
MINOR-ALLIES - [ ] Add checker for
SQUASH-UNRELEASED - [ ] Add checker for
STABLE-RELEASE - [ ] Add checker for
introduced_in
Assumption
- Major/Minor versions are incremental (i.e.: we can't have a version
3if the version2don't exist. Same if version1don't exist)
Change in the json schemes
Some change are required in the json schemes:
- Major revisions of the same command will be contained in the same file in an array.
To simplify the future change, all commands will be put in the an array.
- Introduce field
majors_versions - New field
introduced_inthat replace the fieldintroduced_in_revision
For protocol scheme
type ProtocolScheme = {
label: string,
major_versions: number[],
introduced_in?: MajorMinorString,
req: Request,
reps: Responses,
nested_types?: NestedTypes,
}[]
type Request = RequestWithFields | RequestUnit;
interface RequestWithFields {
cmd: string,
other_fields: Fields,
}
interface RequestUnit {
cmd: string,
other_fields: Fields,
}
interface Fields {
[name: string]: {
type: string,
introduced_in?: MajorMinorString,
}
}
interface Responses {
[status: string]: |
{
other_fields: Fields,
} |
{
unit: string
}
}
interface NestedTypes {
[label: string]: |
// either a enum
{
discriminant_field: string,
variants: Variants,
} |
// or a struct
{
fields: Fields
}
}
interface Variants {
[name: string]: {
discriminant_value: string,
fields: Fields,
}
}
type MajorMinorString = `${Major}.${Minor}`
type Major = number
type Minor = number
Example protocol scheme
[
{
"label": "FooBar",
"major_versions": [
1,
2
],
"req": {
"cmd": "foo_bar",
"other_fields": {
"human_handle": {
"type": "Option<HumanHandle>",
"introduced_in": "1.1"
}
}
},
"reps": {
"ok": {
"other_fields": {}
}
}
},
{
"label": "FooBar",
"major_versions": [
3
],
"req": {
"cmd": "foo_bar",
"other_fields": {
"human_handle": {
"type": "HumanHandle"
}
}
},
"reps": {
"ok": {
"other_fields": {}
},
"error": {
"other_fields": {
"reason": {
"type": "String"
}
}
}
}
}
]
From the example above, we have the field human_handle that is present on versions >=1.1 (including 2.*)
For type scheme
type TypeScheme = {
label: string,
major_versions: number[],
introduced_in?: MajorMinorString,
other_fields: Fields
}[]
Example type scheme
[
{
"label": "FooBarType",
"major_versions": [
1,
2,
],
"other_fields": {
"bar": {
"type": "Int64"
},
"foo": {
"type": "FooType",
"introduced_in": "1.1"
}
}
}
]
From the example above, we have the field foo that's is present on versions >=1.1 (including 2.*)
Testing the scheme
Testing retro-compatibility
We want to verify how a new version is compared to the previous versions.
Testing retro-compatibility on major versions (MAJOR-ENEMIES)
When creating a new major version e.g. 2.0, we MUST check if this version is not compatible with the previous version 1.*
Testing retro-compatibility on minor versions (MINOR-ALLIES)
When creating a new minor version e.g. 1.3, we MUST check if this version is compatible with the previous version 1.2.
Testing multiple unreleased version (SQUASH-UNRELEASED)
During the development process, we may need to edit the api protocol but we don't want to have multiple unreleased version.
If we haven't release the version 2.1, we don't need to create the version 2.2.
Testing readonly older released version (STABLE-RELEASE)
We want to check if we aren't editing a previously released api version.
For that we will need to have a list of released versions and check if those weren't edited
Testing introduced_in
We MUST check the value in introduced_in, the value must respond to the following criteria:
- The value MUST be valid for the type
MajorMinorString - The
majorpart MUST be listed inmajor_versions - The
minorpart MUST be>0
Ensure that minor versions are compatible to each other (i.e.: v1.1 with v1.3).
Just to be sure: we don't need to test v1.1 against v1.3, (but only v1.1 against v1.2, then v1.2 against v1.3)
I guess it's the same thing when test a major version: considering we have versions v1.0 v1.1 v2.0 v2.1, we only need to a single v2 version against a single v1 (typically last v2 against last v1) to ensure there is breaking changes between the two
From the example above, we have the field human_handle that is present in version ~=1.1 but not in version >=2.0
That's interesting: it seemed clear to me that "introduced_in": [{ "major": 1, "minor": 1 },] meant this field is present in ~=1.1 and 2.0 (I expected that if the field disappear an a major version 2, this version should have it own full schema and not share it with version 1)
So we may want of a more explicit way to describe this. For instance if the introduced_in only accept a single value (i.e. "introduced_in": { "major": 1, "minor": 1 }, then it's obvious that the field is optional in major version 1 and always present in major version 2 (and if we want to remove the field from major version 2, we must have a separated schema which is explicit)
For protocol scheme
I think the introduced_in field should also be present for nested_types's fields
For type scheme
For the moment we don't have a versionning for type scheme (we consider we should never break compat, otherwise encrypted data would no longer be readable). But we can consider we are currently in major version 1 in order to have a json format similar to what is done for protocol scheme. And obviously tests for type scheme should only allow a v1 major version !
I've updated the issue body:
- I've define the type
Fieldthat is used everywhere, so it's now used innested_types introduced_innow don't take a list, but a fixedmajor/minor
I've updated the issue body:
introduced_inis now astring-pattern- Add step to check the value of
introduced_in
One thing that appear to be missing is how to we separate the different major version of an protocol.
Protocol using enum variant to separate major version.
mod ping {
enum Ping {
V1(PingV1V2),
V2(PingV1V2),
V3(PingV3)
}
struct PingV1V2 {
// ...
}
struct PingV3 {
// ...
}
}
Protocol using sub-module to separate major version.
mod ping {
mod v1v2 {
struct Ping {
// ...
}
// ...
}
mod v1 {
pub use super::v1v2::Ping;
// ...
}
mod v2 {
pub use super::v1v2::Ping;
// ...
}
mod v3 {
struct Ping {
// ...
}
// ...
}
}
Protocols using module to separate major version.
mod v1 {
mod ping {
struct Ping {
// ...
}
// ...
}
// ...
}
mod v2 {
mod ping {
struct Ping {
// ...
}
// ...
}
// ...
}
mod v3 {
mod ping {
struct Ping {
// ...
}
// ...
}
// ...
}
So it appear that the proposal Protocol using module to separate major version. is preferred
I've updated the original issue with the following change:
- Rename
TypeSchemethat was previously calledProtocolSchemedue to a bad copy/paste. - Added the optional field
introduced_inforProtocolScheme&TypeScheme. This field only server for documentation purpose for when a scheme is added during minor bump
I've update the original issue with the following change:
- Rework the type definition of
Protocol&Typeto prefer the use of key/value instead of array keyed by an internal value. This change is to prevent by design duplication of values - Rework the examples according to the change above