File: index.md

package info (click to toggle)
storm-lang 0.7.5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 52,100 kB
  • sloc: ansic: 261,471; cpp: 140,438; sh: 14,891; perl: 9,846; python: 2,525; lisp: 2,504; asm: 860; makefile: 678; pascal: 70; java: 52; xml: 37; awk: 12
file content (280 lines) | stat: -rw-r--r-- 11,056 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
JSON Library
============

The package `json` contains a library for serialization, deserialization, and manipulation of JSON
data. The central part of the library is the class [stormname:json.JsonValue] that represents a JSON
value.


JSON Values
-----------

As mentioned above, the class [stormname:json.JsonValue] is used to represent arbitrary JSON values.
An instance of `JsonValue` can be thought of as a variant that is specialized for JSON usage. In
particular, it implements *both* the interface for an array and for a map, so that it is convenient
to access the data without explicit typechecking. Furthermore, a `JsonValue` that is created with
its default constructor is initially empty (i.e. `null`), but can become any type if you try to add
data to it. This only works once, however. Once the `JsonValue` contains a value, it will throw an
exception if used improperly.

### Creating Values

Creating a `JsonValue` that contains a scalar type (i.e. booleans, numbers, and strings) is done
using one of its constructors. These constructors are marked cast constructors to allow the
conversion to happen implicitly. Therefore, it is possible to do the following in Basic Storm:

```bsstmt
JsonValue b = false;
JsonValue n = 32;
JsonValue d = 3.5;
JsonValue s = "test";
```

To create arrays or maps, the most convenient way is to create an empty `JsonValue` and start
populating it. Note that `JsonValue` replicates the interface of `Map<T>` and `Array<T>`. There are
also constructors that accept `JsonValue[]` and `Str->JsonValue` respectively for programs that
produce the contents separately.

```bsstmt
JsonValue array;
array << 20 << 30;

JsonValue object;
object["key"] = "value";
object["number"] = 12;
// or
object.put("key", "value");
object.put("number", 12);
```

Note that `array` and `object` above will both have the value `null` before elements are added to
them. If you wish to create empty arrays and objects, you can use the static functions
[stormname:json.JsonValue.emptyArray] and [stormname:json.JsonValue.emptyObject]. This is mainly a
concern if you need to serialize the representation into empty objects or arrays down the line.


### Inspecting Values

Inspecting the contents of a `JsonValue` is similarly aimed at being similar to existing
conventions, but without requiring explicit checks all the time. Instead, the API is designed to
throw exceptions whenever the expectations of the program do not match the structure.

The first part of inspecting the contents of a `JsonValue` are the typecast members. As with many
other types in the standard library, these are named after the type casted to. All of these throw an
exception if the `JsonValue` does not contain the expected type. They are as follows:

- `byte`, `int`, `nat`, `long`, `word`

  Extract an integer type. Note that this only works for types that were integer types from the
  start (e.g. in the serialization source). Creating a `JsonValue` from a floating point value and
  trying to extract an integer type will result in an error. Also note that values are stored
  internally as [stormname:core.Long].

  You can check if the `JsonValue` contains an integer number using `isInteger`.

- `float`, `double`

  Extract a floating point type. Note that integer types are automatically converted to floating
  point types if required. Values are stored internally as [stormname:core.Double].

  You can check if the `JsonValue` contains a floating point number using `isNumber`. Note that if
  the value contains an integer, both `isNumber` and `isInteger` will return true.

- `str`

  Extract a [stormname:core.Str]. You can check if the value contains a string using `isStr`.

- `array`

  Extract an array of contained elements. In general, this is only necessary when you wish to
  iterate through the contents of the container. The number of elements can be retrieved using
  `count`. You can check if the value contains a string using `isArray`.

- `object`

  Extract a map of contained elements. In general, this is only necessary when you wish to iterate
  through the contents of the container. The number of elements can be retrieved using `count`. You
  can check if the value contains a string using `isObject`.


For arrays and objects, `JsonValue` additionally implements the interface for arrays and maps. As
such, it is possible to access elements using the appropriate operators on the value directly. There
is, however, one minor difference regarding the behavior of `[]` for objects. Namely, that `[]`
behaves like `get` in that it throws an exception when trying to read a key that does not exist.

Using this API, a JSON object can be inspected as below:

```bsstmt
JsonValue array;
array << 20 << 20.5;

JsonValue object;
object["a"] = "string";
object["b"] = array;

for (k, v in object.object) {
    print("${k} -> ${v}");
}

Str aValue = object["a"].str;
JsonValue bValue = object["b"];

for (id, v in bValue.array) {
    print("${id}: ${v}");
}

Double first = bValue[0].double;
Double second = bValue[1].double;
```

The [stormname:json.JsonValue] also contains a `==` operator to allow comparing arbitrary JSON
values. It implements a deep comparison.

### JSON Literals

The library also provides a syntax extension that allows embedding JSON literals into Basic Storm
code. Literals start with the keyword `json` and continues with either an object or an array using
the standard JSON syntax. All parts of the JSON hierarchy can be replaced by arbitrary Basic Storm
expressions except for the keys in object literals.

Keys in JSON objects do not have to be enclosed in quotes if it only contains alphanumeric
characters and underscores (which is otherwise required by JSON). As such, to use Basic Storm
expressions in keys, either use the interpolated string syntax (i.e. `"${expr}"`) or enclose the
expression in `${expr}`.

Below is an example of a json literal. Note that it captures values from the surrounding code.

```bsstmt:use=json
Str s = "string";
Int i = 15;
Str name = "keyname";
JsonValue value = json{
   "normal key": "value",
   unquoted-key: s,
   array: [s, i, name],
   "${name}": "key is named 'keyname'",
   ${name + "!"}: "key is named 'keyname!'",
};
```

Serialization
-------------

The [stormname:json.JsonValue] class serializes JSON to a proper string representation using its
`toS` method as usual. By default, the `toS` generates formatted JSON documents, using line breaks
and indentation to make it easier to read the structure.

The class provides overloads to change the formatting options. If one parameter is passed to `toS`,
it indicates the indentation depth (in number of spaces). If zero is passed, it produces a single
line, compact representation. A second parameter to `toS` indicates if keys in objects should be
sorted alphabetically. Since `Map<T>` does not preserve the insertion order of elements in Storm,
element ordering in objects is otherwise unpredictable.

Similar options are available for the `toS` overload that accepts a `StrBuf` as its first parameter.
However, the second parameter is a boolean that instructs if the compact representation should be
used rather than a number since this overload uses the `StrBuf`'s standard indentation mechanism.

The output from the serialization is always ASCII (i.e. non-ascii characters are escaped). As such,
it can be converted to binary data using [stormname:core.io.toUtf8(core.Str)] without issues.

Deserialization is provided via the function `parseJson`. There are two overloads, one that accepts
a string and another that accepts a [stormname:core.io.Buffer]. The second one allows parsing
UTF-8-encoded binary data before decoding it first.


Exceptions
----------

All exceptions thrown by the JSON library inherit from [stormname:json.JsonError]. There are two
subtypes, [stormname:json.JsonParseError] that is thrown by the JSON parser, and
[stormname:json.JsonAccessError] that is thrown on incorrect accesses to the [stormname:json.JsonValue]
class. The latter of the two also captures a stacktrace to ease debugging.


Object Serialization
--------------------

The library additionally contains the decorator `jsonSerializable` that automatically generates code
that converts between [stormname:json.JsonValue] and regular Storm classes to make it easier to
consume and produce JSON data in a structured manner. This is not too dissimilar from the normal
[serialization mechanism](md:/Library_Reference/Standard_Library/IO/Serialization).

To illustrate how the decorator works, consider the following class:

```bs
class Employee : jsonSerializable {
    Str name;
    Nat salary = 500;
    Str? speciality;
}
```

In this case, the `jsonSerializable` decorator adds the following members to the class:

```bs
class Employee : jsonSerializable {
    Str name;
    Nat salary = 500;
    Str? speciality;

    init(JsonValue json) {
        init {
            name = json["name"].str;
            salary = if (value = json.at("salary")) {
                value.nat;
            } else {
                500;
            };
            speciality = {
                element = json["speciality"];
                if (element.isNull) {
                    Str?();
                } else {
                    Str?(element.str);
                }
            };
        }
    }

    JsonValue toJson() {
        var out = JsonValue:emptyObject();
        out.put("name", JsonValue(name));
        out.put("salary", JsonValue(salary));
        if (speciality) {
            out.put("speciality", JsonValue(speciality));
        } else {
            out.put("speciality", JsonValue());
        }
        return out;
    }
}
```

As we can see, `jsonSerializable` adds a constructor that converts a `JsonValue` into the type, as
well as a `toJson` function that converts the type into JSON. As such, we can use the functions as
follows:

```bsstmt:use=json
var src = json{
    "name": "Test",
    "salary": 1000,
    "speciality": null
};
var converted = src.Employee;
// or
var converted = Employee(src);
```

To convert back, we can of course just call `converted.toJson`.

It is worth noting that the deserialization will only allow members that have explicit default
values set to be missing from the JSON. For example, `salary` is allowed to be missing, while
`speciality` is not allowed to, even though it is a maybe type.

Finally, even though it is not illustrated above, the serialization library supports serializing and
deserializing other types that have an appropriate constructor and a `toJson` function. It also
supports arrays, maps with string keys, and maybe types natively. Inheritance is also supported, but
since the actual type of an object is not stored in the JSON representation, the support is not as
robust as the serialization library in Storm. That is, if we would serialize a subclass to
`Employee` using its `toJson`, we will always get `Employee` if we deserialize it using
`json.Employee()` since the system does not know which subclass was originally serialized.