Skip to content

Commit

Permalink
feat(compiler): support json to struct conversion (#3648)
Browse files Browse the repository at this point in the history
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
hasanaburayyan authored Aug 12, 2023
1 parent 9399564 commit a930aa4
Show file tree
Hide file tree
Showing 30 changed files with 2,153 additions and 27 deletions.
37 changes: 33 additions & 4 deletions docs/docs/03-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ str.fromJson(jsonNumber); // RUNTIME ERROR: unable to parse number `123` as
num.fromJson(Json "\"hello\""); // RUNTIME ERROR: unable to parse string "hello" as a number
```
For each `fromJson()`, there is a `tryFromJson()` method which returns an optional `T?` which
indicates if parsing was successful or not:
```js
let s = str.tryFromJson(myJson) ?? "invalid string";
``````
##### 1.1.4.6 Mutability
To define a mutable JSON container, use the `MutJson` type:
Expand Down Expand Up @@ -335,7 +341,31 @@ Json.delete(immutObj, "hello");
// ^^^^^^^^^ expected `JsonMut`
```
##### 1.1.4.7 Serialization
##### 1.1.4.7 Assignment to user-defined structs
All [structs](#31-structs) also have a `fromJson()` method that can be used to parse `Json` into a
struct:
```js
struct Contact {
first: str;
last: str;
phone: str?;
}
let j = Json { first: "Wing", last: "Lyly" };
let myContact = Contact.fromJson(j);
assert(myContact.first == "Wing");
```
When a `Json` is parsed into a struct, the schema will be validated to ensure the result is
type-safe:
```js
let p = Json { first: "Wing", phone: 1234 };
Contact.fromJson(p);
// RUNTIME ERROR: unable to parse Contact:
// - field "last" is required and missing
// - field "phone" is expected to be a string, got number.
```
##### 1.1.4.8 Serialization
The `Json.stringify(j: Json): str` static method can be used to serialize a `Json` as a string
([JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)):
Expand All @@ -360,7 +390,7 @@ let boom = num.fromJson(j.get("boom"));
let o = Json.tryParse("xxx") ?? Json [1,2,3];
```
##### 1.1.4.8 Logging
##### 1.1.4.9 Logging
A `Json` value can be logged using `log()`, in which case it will be pretty-formatted:
Expand All @@ -378,13 +408,12 @@ my object is: {
}
```
#### 1.1.4.9 Roadmap
#### 1.1.4.10 Roadmap
The following features are not yet implemented, but we are planning to add them in the future:
* Array/Set/Map.fromJson() - see https://github.com/winglang/wing/issues/1796 to track.
* Json.entries() - see https://github.com/winglang/wing/issues/3142 to track.
* Schema validation and assignment to struct - see https://github.com/winglang/wing/issues/3139 to track.
* Equality, diff and patch - see https://github.com/winglang/wing/issues/3140 to track.
[`▲ top`][top]
Expand Down
44 changes: 44 additions & 0 deletions docs/docs/04-standard-library/02-std/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,50 @@ The length of the string.
---


### Struct <a name="Struct" id="@winglang/sdk.std.Struct"></a>

Shared behavior for all structs.


#### Static Functions <a name="Static Functions" id="Static Functions"></a>

| **Name** | **Description** |
| --- | --- |
| <code><a href="#@winglang/sdk.std.Struct.fromJson">fromJson</a></code> | Converts a Json to a Struct. |
| <code><a href="#@winglang/sdk.std.Struct.tryFromJson">tryFromJson</a></code> | Converts a Json to a Struct, returning nil if the Json is not valid. |

---

##### `fromJson` <a name="fromJson" id="@winglang/sdk.std.Struct.fromJson"></a>

```wing
Struct.fromJson(json: Json);
```

Converts a Json to a Struct.

###### `json`<sup>Required</sup> <a name="json" id="@winglang/sdk.std.Struct.fromJson.parameter.json"></a>

- *Type:* <a href="#@winglang/sdk.std.Json">Json</a>

---

##### `tryFromJson` <a name="tryFromJson" id="@winglang/sdk.std.Struct.tryFromJson"></a>

```wing
Struct.tryFromJson(json: Json);
```

Converts a Json to a Struct, returning nil if the Json is not valid.

###### `json`<sup>Required</sup> <a name="json" id="@winglang/sdk.std.Struct.tryFromJson.parameter.json"></a>

- *Type:* <a href="#@winglang/sdk.std.Json">Json</a>

---



## Structs <a name="Structs" id="Structs"></a>

### DatetimeComponents <a name="DatetimeComponents" id="@winglang/sdk.std.DatetimeComponents"></a>
Expand Down
13 changes: 13 additions & 0 deletions examples/tests/error/struct_from_json_1.w
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
25 changes: 25 additions & 0 deletions examples/tests/error/struct_from_json_2.w
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"
29 changes: 29 additions & 0 deletions examples/tests/error/struct_from_json_3.w
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
29 changes: 29 additions & 0 deletions examples/tests/error/struct_from_json_4.w
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
18 changes: 18 additions & 0 deletions examples/tests/error/struct_from_json_5.w
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
23 changes: 23 additions & 0 deletions examples/tests/invalid/struct_json_conversion.w
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
Loading

0 comments on commit a930aa4

Please sign in to comment.