forked from SquareBracketAssociates/EnterprisePharo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
NeoJSON.pier
247 lines (168 loc) · 8.36 KB
/
NeoJSON.pier
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
! JSON
@cha:JSON
JSON (JavaScript Object Notation) is a popular data-interchange format. NeoJSON is an elegant and efficient standalone Smalltalk library to read and write
JSON converting to and from Smalltalk objects. The library is developed and actively maintained by Sven Van Caekenberghe.
!!An introduction to JSON
JSON is a lightweight text-based open standard designed for human-readable data interchange. It was derived from the JavaScript scripting language for
representing simple data structures and associative arrays, called objects. Despite its relationship to JavaScript, it is language-independent, with parsers
available for many languages.
@@note References: *http://www.json.org/*, *http://en.wikipedia.org/wiki/Json* and *http://www.ietf.org/rfc/rfc4627.txt?number=4627*.
There are only a couple of primitive types in JSON:
-numbers (integer or floating point)
-strings
-the boolean constants ==true== and ==false==
- ==null==
Only two composite types exist:
-lists (an ordered sequenece of values)
-maps (an unordered associative array, mapping string property names to values)
That is really all there is to it. No options or additions are defined in the standard.
!! NeoJSON
To load NeoJSON, evaluate the following:
[[[language=smalltalk
Gofer it
smalltalkhubUser: 'SvenVanCaekenberghe' project: 'Neo';
configurationOf: 'NeoJSON';
loadStable.
]]]
The NeoJSON library contains a reader (==NeoJSONReader==) and a writer (==NeoJSONWriter==) to parse, respectively generate, JSON to and from Pharo objects. The goals of NeoJSON are:
-to be standalone (have no dependencies and little requirements);
-to be small, elegant and understandable;
-to be efficient (both in time and space);
-to be flexible and non-intrusive.
Compared to other Smalltalk JSON libraries, NeoJSON
-has less dependencies and little requirements;
-can be more efficient (be faster and use less memory);
-allows for the use of schemas and mappings.
!!Primitives
Obviously, the primitive types are mapped to corresponding Pharo classes. While reading:
- JSON numbers become instances of ==Integer== or ==Float==
- JSON strings become instances of ==String==
- JSON booleans become instances of ==Boolean==
- JSON ==null== becomes ==nil==
While writing:
- Pharo numbers are converted to floats, except for instances of ==Integer== that become JSON integers;
- Pharo strings become JSON strings;
- Pharo booleans become JSON booleans;
- Pharo ==nil== becomes JSON ==null==.
!! Generic Mode
NeoJSON can operate in a generic mode that requires no further configuration.
!!! Reading from JSON
While reading:
-JSON maps become instances of mapClass, ==Dictionary== by default;
-JSON lists become instances of listClass, ==Array== by default.
The following example creates a Pharo array from a JSON expression:
[[[language=smalltalk
NeoJSONReader fromString: ' [ 1,2,3 ] '.
]]]
This expression can be decomposed to better control the reading process:
[[[language=smalltalk
(NeoJSONReader on: ' [ 1,2,3 ] ' readStream)
listClass: OrderedCollection;
next.
]]]
The above expression is equivalent to the previous one except that a Pharo ordered collection will be used in place of an array.
The next example creates a Pharo dictionary (with =='x'== and =='y'== keys):
[[[language=smalltalk
NeoJSONReader fromString: ' { "x" : 1, "y" : 2 } '.
]]]
To automatically convert keys to symbols, pass ==true== to ==propertyNamesAsSymbols:== like this:
[[[language=smalltalk
(NeoJSONReader on: ' { "x" : 1, "y" : 2 } ' readStream)
propertyNamesAsSymbols: true;
next
]]]
The result of this expression is a dictionary with ==#x== and ==#y== as keys.
!!!Writing to JSON
While writing:
-instances of ==Dictionary== and ==SmallDictionary== become maps;
-all other collections become lists;
-all other non-primitive objects are rejected.
Here are some examples writing in generic mode:
[[[language=smalltalk
NeoJSONWriter toString: #(1 2 3).
NeoJSONWriter toString: { Float pi. true. false. 'string' }.
NeoJSONWriter toString: { #a -> '1' . #b -> '2' } asDictionary.
]]]
Above expressions return a compact string (''i.e.'', with neither indentation nor new lines). To get a nicely formatted output, use ==toStringPretty:== like this:
[[[language=smalltalk
NeoJSONWriter toStringPretty: #(1 2 3).
]]]
In order to use the generic mode, you have to convert your domain objects to and from ==Dictionary== and ==SequenceableCollection==. This is relatively easy but not very efficient, depending on the use case.
!! Schemas and Mappings
NeoJSON allows for the optional specification of schemas and mappings to be used when writing or reading.
When writing, mappings are used when arbitrary objects are seen. For example, in order to write an array of points, you could do as follows:
[[[language=smalltalk
String streamContents: [ :stream |
(NeoJSONWriter on: stream)
prettyPrint: true;
mapInstVarsFor: Point;
nextPut: (Array with: 1@3 with: -1@3) ].
]]]
Collections are handled automatically, like in the generic case.
When reading, a mapping is used to specify what Pharo object to instantiate and how to instantiate it. Here is a very simple case, reading a map as a point:
[[[
(NeoJSONReader on: ' { "x" : 1, "y" : 2 } ' readStream)
mapInstVarsFor: Point;
nextAs: Point.
]]]
Since JSON lacks a universal way to specify the class of an object, we have to specify the target schema that we want to use as an argument to ==nextAs:==.
To define the schema of the elements in a list, write something like the following:
[[[
(NeoJSONReader on: ' [{ "x" : 1, "y" : 2 }, { "x" : 3, "y" : 4 }] ' readStream)
mapInstVarsFor: Point;
for: #ArrayOfPoints customDo: [ :mapping | mapping listOfElementSchema: Point ];
nextAs: #ArrayOfPoints.
]]]
The above expression returns an array of 2 points. As you can see, the argument to ==nextAs:== can be a class (as seen previously) be also any symbol, provided the mapper knows about it.
To get an ==OrderedCollection== instead of an array as output, use the ==listOfType:== message:
[[[language=smalltalk
(NeoJSONReader on: ' [ 1, 2 ] ' readStream)
for: #Collection customDo: [ :mapping | mapping listOfType: OrderedCollection ];
nextAs: #Collection.
]]]
To specify how values in a map should be instantiated, use the ==mapWithValueSchema:==:
[[[language=smalltalk
(NeoJSONReader on: ' { "point1" : {"x" : 1, "y" : 2 } }' readStream)
mapInstVarsFor: Point;
for: #DictionaryOfPoints
customDo: [ :mapping | mapping mapWithValueSchema: Point ];
nextAs: #DictionaryOfPoints.
]]]
The above expression returns a ==Dictionary== with 1 key-value pair =='point1' -> (1@2)==.
You can go beyond pre-defined messages and specify a decoding block:
[[[language=smalltalk
(NeoJSONReader on: ' "2015/06/19" ' readStream)
for: DateAndTime
customDo: [ :mapping | mapping decoder: [ :string | DateAndTime fromString: string ] ];
nextAs: DateAndTime.
]]]
The above expression returns an instance of ==DateAndTime==. The message ==encoder:== is used to do the opposite, ''i.e.'' convert from a Smalltalk object to JSON:
[[[language=smalltalk
String streamContents: [ :stream |
(NeoJSONWriter on: stream)
for: DateAndTime customDo: [ :mapping | mapping encoder: #printString ];
nextPut: DateAndTime now ].
]]]
The above expression returns a string representing the current date and time.
NeoJSON deals efficiently with mappings: the minimal amount of intermediary structures are created.
On modern hardware, NeoJSON can write or read tens of thousands of small objects per second. Several benchmarks are included in the unit tests package.
!!Emitting null values
For efficiency reasons, by default, ==NeoJSONWriter== does not write ==nil== values:
[[[language=smalltalk
String streamContents: [ :stream |
(NeoJSONWriter on: stream)
mapAllInstVarsFor: Point;
nextPut: Point new ].
]]]
The above expression returns the =='{}'== string. If you want to see the uninitialized instance properties, pass ==true== to the ==writeNil:== message:
[[[language=smalltalk
String streamContents: [ :stream |
(NeoJSONWriter on: stream)
mapAllInstVarsFor: Point;
writeNil: true;
nextPut: Point new ].
]]]
The above expression returns the =='{"x":null,"y":null}'== string.
!! Conclusion
NeoJSON is a powerful library to convert objects. Sven, the author of NeoJSON, also developed STON (Smalltalk object notation) which is closer to Pharo syntax and handles cycles and references between serialized objects.
% LocalWords: Caekenberghe