-
Notifications
You must be signed in to change notification settings - Fork 202
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compiler): support json to struct conversion (#3648)
The changes in this PR make it possible to call `fromJson` on any compatible struct definition. ## Implementation notes: Previously we treated the jsification of structs as a no-op since there is not a JS equivalent. So with this change structs now JSify into a `Struct` file that contains a class with a static `jsonSchema` and a `fromJson` function which will allow for field validation at runtime. The schema generated adheres to: https://json-schema.org/understanding-json-schema/ take this simple example Wing code: ```js struct MyStruct { myField: str; myOtherField: num; } ``` this will now generate a JS file named `MyStruct.Struct.js` which looks like this: ```js module.exports = function(stdStruct, fromInline) { class MyStruct { static jsonSchema() { return { id: "/MyStruct", type: "object", properties: { myField: { type: "string" }, myOtherField: { type: "number" }, }, required: [ "myField", "myOtherField", ], $defs: { } } } static fromJson(obj) { return stdStruct._validate(obj, this.jsonSchema()) } static _toInflightType(context) { return fromInline(`require("./MyStruct.Struct.js")(${ context._lift(stdStruct) })`); } } return MyStruct; }; ``` The piece that brings this all together is the addition of the `Struct` class in our std that only has a `fromJson()` methods at the moment that is a Wing macro. The macro just calls the `fromJson()` method in the generated javascript. ### Misc We want to stop the user at compile time from calling `fromJson` on a struct that cannot be represented by a Json value ie ```js struct MyStruct { b: cloud.Bucket; } let j = {}; MyStruct.fromJson(j); ``` to prevent this I added a check in the typechecker for structs to confirm that if `fromJson` is called that all the fields in the struct are valid for conversion attempt See image below for error when attempting: <img width="664" alt="image" src="https://github.com/winglang/wing/assets/45375125/785a2fa6-8823-4fa2-aaa5-4bc8f7ed597f"> Closes: #3653 Closes: #3139 ## TODO: - [x] separate the work done here and the remaining work into different tickets. - [x] update language reference ## Followup issues that are out of scope for this PR: - #3792 - #3790 - #3789 - #3788 ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [x] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
- Loading branch information
1 parent
9399564
commit a930aa4
Showing
30 changed files
with
2,153 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// Note that this test has to be alone because it needs to compile successfully and fail at preflight. | ||
// If it is run with other tests, subsequent failures will be ignored in snapshot. | ||
|
||
struct Person { | ||
name: str; | ||
age: num; | ||
} | ||
|
||
let j = {name: "cool", age: "not a number"}; | ||
|
||
Person.fromJson(j); | ||
// ^ ERROR: unable to parse Person: | ||
// - instance.age is not of a type(s) number |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// Note that this test has to be alone because it needs to compile successfully and fail at preflight. | ||
// If it is run with other tests, subsequent failures will be ignored in snapshot. | ||
|
||
struct Person { | ||
name: str; | ||
age: num; | ||
} | ||
|
||
struct Advisor extends Person { | ||
id: str; | ||
} | ||
|
||
struct Student extends Person { | ||
advisor: Advisor; | ||
} | ||
|
||
let missingAdvisor = { | ||
name: "cool", | ||
age: "not a number" | ||
}; | ||
|
||
Student.fromJson(missingAdvisor); | ||
// ^ ERROR: unable to parse Student: | ||
// - instance.age is not of a type(s) number | ||
// - instance requires property "advisor" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Note that this test has to be alone because it needs to compile successfully and fail at preflight. | ||
// If it is run with other tests, subsequent failures will be ignored in snapshot. | ||
|
||
struct Person { | ||
name: str; | ||
age: num; | ||
} | ||
|
||
struct Advisor extends Person { | ||
id: str; | ||
} | ||
|
||
struct Student extends Person { | ||
advisors: Array<Advisor>; | ||
} | ||
|
||
let invalidAdvisorInArray = { | ||
name: "cool", | ||
age: "not a number", | ||
advisors: [ | ||
{id: "advisor1", name: "Bob", age: 34}, | ||
{id: 10, name: "Jacob", age: 45} | ||
] | ||
}; | ||
|
||
Student.fromJson(invalidAdvisorInArray); | ||
// ^ ERROR: unable to parse Student: | ||
// - instance.age is not of a type(s) number | ||
// - instance.advisors[1].id is not of a type(s) string |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Note that this test has to be alone because it needs to compile successfully and fail at preflight. | ||
// If it is run with other tests, subsequent failures will be ignored in snapshot. | ||
|
||
struct Person { | ||
name: str; | ||
age: num; | ||
} | ||
|
||
struct Advisor extends Person { | ||
id: str; | ||
} | ||
|
||
struct Student extends Person { | ||
advisors: Set<Advisor>; // <== Using Set instead of Array | ||
} | ||
|
||
// Try adding two of the same adivsor | ||
let invalidAdvisorInArray = { | ||
name: "cool", | ||
age: 22, | ||
advisors: [ | ||
{id: "advisor1", name: "Bob", age: 34}, | ||
{id: "advisor1", name: "Bob", age: 34}, | ||
] | ||
}; | ||
|
||
Student.fromJson(invalidAdvisorInArray); | ||
// ^ ERROR: unable to parse Student: | ||
// - instance.advisors contains duplicate item |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Note that this test has to be alone because it needs to compile successfully and fail at preflight. | ||
// If it is run with other tests, subsequent failures will be ignored in snapshot. | ||
|
||
struct Foo { | ||
names: Map<str>; | ||
} | ||
|
||
let jFoo = { | ||
names: { | ||
a: "Amanda", | ||
b: "Barry", | ||
c: 10 | ||
} | ||
}; | ||
|
||
Foo.fromJson(jFoo); | ||
// ^ ERROR: unable to parse Foo: | ||
// - instance.names.c is not of a type(s) string |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
bring cloud; | ||
|
||
struct A { | ||
a: str; | ||
b: cloud.Bucket; | ||
} | ||
|
||
A.fromJson({}); | ||
//^^^^^^^^ Struct "A" contains field "b" which cannot be represented in Json | ||
|
||
struct B { | ||
a: A; | ||
} | ||
|
||
B.fromJson({}); | ||
//^^^^^^^^ Struct "B" contains field "a" which cannot be represented in Json | ||
|
||
struct C extends A { | ||
c: num; | ||
} | ||
|
||
C.fromJson({}); | ||
//^^^^^^^^ Struct "C" contains field "b" which cannot be represented in Json |
Oops, something went wrong.