Skip to content

Commit c43ab16

Browse files
committed
chore: add description for auto reconstruct by json schema
1 parent 868f663 commit c43ab16

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed

AUTO_RECONSCTRUCT_BY_JSON_SCHEME.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Auto reconstruct by json schema
2+
## Problem Solved: Could not decode contract state to class instance in early version of sdk
3+
JS SDK decode contract as utf-8 and parse it as JSON, results in a JS Object.
4+
One thing not intuitive is objects are recovered as Object, not class instance. For example, Assume an instance of this class is stored in contract state:
5+
```typescript
6+
Class Car {
7+
name: string;
8+
speed: number;
9+
10+
run() {
11+
// ...
12+
}
13+
}
14+
```
15+
When load it back, the SDK gives us something like:
16+
```json
17+
{"name": "Audi", "speed": 200}
18+
```
19+
However this is a JS Object, not an instance of Car Class, and therefore you cannot call run method on it.
20+
This also applies to when user passes a JSON argument to a contract method. If the contract is written in TypeScript, although it may look like:
21+
```typescript
22+
add_a_car(car: Car) {
23+
car.run(); // doesn't work
24+
this.some_collection.set(car.name, car);
25+
}
26+
```
27+
But car.run() doesn't work, because SDK only know how to deserialize it as a plain object, not a Car instance.
28+
This problem is particularly painful when class is nested, for example collection class instance LookupMap containing Car class instance. Currently SDK mitigate this problem by requires user to manually reconstruct the JS object to an instance of the original class.
29+
## A method to decode string to class instance by json schema file
30+
we just need to add static member in the class type.
31+
```typescript
32+
Class Car {
33+
static schema = {
34+
name: "string",
35+
speed: "number",
36+
};
37+
name: string;
38+
speed: number;
39+
40+
run() {
41+
// ...
42+
}
43+
}
44+
```
45+
After we add static member in the class type in our smart contract, it will auto reconstruct smart contract and it's member to class instance recursive by sdk.
46+
And we can call class's functions directly after it deserialized.
47+
```js
48+
add_a_car(car: Car) {
49+
car.run(); // it works!
50+
this.some_collection.set(car.name, car);
51+
}
52+
```
53+
### The schema format
54+
#### We support multiple type in schema:
55+
* build-in non object types: `string`, `number`, `boolean`
56+
* build-in object types: `date`, `bigint`
57+
* build-in collection types: `array`, `map`
58+
* for `array` type, we need to declare it in the format of `{array: {value: valueType}}`
59+
* for `map` type, we need to declare it in the format of `{map: {key: 'KeyType', value: 'valueType'}}`
60+
* Custom Class types: `Car` or any class types
61+
* Near collection types: `Vector`, `LookupMap`, `LookupSet`, `UnorderedMap`, `UnorderedSet`
62+
*
63+
We have a test example which contains all those types in one schema: [status-deserialize-class.js](./examples/src/status-deserialize-class.js)
64+
```js
65+
class StatusDeserializeClass {
66+
static schema = {
67+
is_inited: "boolean",
68+
records: {map: {key: 'string', value: 'string'}},
69+
car: Car,
70+
messages: {array: {value: 'string'}},
71+
efficient_recordes: {unordered_map: {value: 'string'}},
72+
nested_efficient_recordes: {unordered_map: {value: {unordered_map: {value: 'string'}}}},
73+
nested_lookup_recordes: {unordered_map: {value: {lookup_map: {value: 'string'}}}},
74+
vector_nested_group: {vector: {value: {lookup_map: {value: 'string'}}}},
75+
lookup_nest_vec: {lookup_map: {value: {vector: {value: 'string'}}}},
76+
unordered_set: {unordered_set: {value: 'string'}},
77+
user_car_map: {unordered_map: {value: Car}},
78+
big_num: 'bigint',
79+
date: 'date'
80+
};
81+
82+
constructor() {
83+
this.is_inited = false;
84+
this.records = {};
85+
this.car = new Car();
86+
this.messages = [];
87+
// account_id -> message
88+
this.efficient_recordes = new UnorderedMap("a");
89+
// id -> account_id -> message
90+
this.nested_efficient_recordes = new UnorderedMap("b");
91+
// id -> account_id -> message
92+
this.nested_lookup_recordes = new UnorderedMap("c");
93+
// index -> account_id -> message
94+
this.vector_nested_group = new Vector("d");
95+
// account_id -> index -> message
96+
this.lookup_nest_vec = new LookupMap("e");
97+
this.unordered_set = new UnorderedSet("f");
98+
this.user_car_map = new UnorderedMap("g");
99+
this.big_num = 1n;
100+
this.date = new Date();
101+
}
102+
// other methods
103+
}
104+
```
105+
#### no need to announce GetOptions.reconstructor in decoding nested collections
106+
In this other hand, after we set schema for the Near collections with nested collections, we don't need to announce `reconstructor` when we need to get and decode a nested collections because the data type info in the schema will tell sdk what the nested data type.
107+
Before we set schema if we need to get a nested collection we need to set `reconstructor` in `GetOptions`:
108+
```typescript
109+
@NearBindgen({})
110+
export class Contract {
111+
outerMap: UnorderedMap<UnorderedMap<string>>;
112+
113+
constructor() {
114+
this.outerMap = new UnorderedMap("o");
115+
}
116+
117+
@view({})
118+
get({id, accountId}: { id: string; accountId: string }) {
119+
const innerMap = this.outerMap.get(id, {
120+
reconstructor: UnorderedMap.reconstruct, // we need to announce reconstructor explicit
121+
});
122+
if (innerMap === null) {
123+
return null;
124+
}
125+
return innerMap.get(accountId);
126+
}
127+
}
128+
```
129+
After we set schema info we don't need to set `reconstructor` in `GetOptions`, sdk can infer which reconstructor should be took by the schema:
130+
```typescript
131+
@NearBindgen({})
132+
export class Contract {
133+
static schema = {
134+
outerMap: {unordered_map: {value: { unordered_map: {value: 'string'}}}}
135+
};
136+
137+
outerMap: UnorderedMap<UnorderedMap<string>>;
138+
139+
constructor() {
140+
this.outerMap = new UnorderedMap("o");
141+
}
142+
143+
@view({})
144+
get({id, accountId}: { id: string; accountId: string }) {
145+
const innerMap = this.outerMap.get(id, {
146+
reconstructor: UnorderedMap.reconstruct, // we need to announce reconstructor explicit, reconstructor can be infered from static schema
147+
});
148+
if (innerMap === null) {
149+
return null;
150+
}
151+
return innerMap.get(accountId);
152+
}
153+
}
154+
```

examples/src/status-deserialize-class.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Car {
2727
@NearBindgen({})
2828
export class StatusDeserializeClass {
2929
static schema = {
30-
is_inited: "bool",
30+
is_inited: "boolean",
3131
records: {map: { key: 'string', value: 'string' }},
3232
car: Car,
3333
messages: {array: {value: 'string'}},

pnpm-lock.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)