Skip to content

Commit 02e0b87

Browse files
committed
Refactoring for better REST and JSON support
1 parent 99a6b02 commit 02e0b87

File tree

6 files changed

+430
-167
lines changed

6 files changed

+430
-167
lines changed

convex-core/src/main/java/convex/core/util/JSONUtils.java

Lines changed: 226 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22

33
import java.util.ArrayList;
44
import java.util.HashMap;
5+
import java.util.Iterator;
56
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Map.Entry;
69

710
import convex.core.cvm.Address;
811
import convex.core.data.ACell;
12+
import convex.core.data.ACollection;
913
import convex.core.data.AMap;
1014
import convex.core.data.ASequence;
1115
import convex.core.data.AString;
16+
import convex.core.data.ASymbolic;
17+
import convex.core.data.Blob;
1218
import convex.core.data.Keyword;
1319
import convex.core.data.MapEntry;
20+
import convex.core.data.StringShort;
1421
import convex.core.data.Strings;
1522
import convex.core.data.prim.CVMBool;
1623
import convex.core.data.prim.CVMChar;
@@ -62,7 +69,7 @@ public static <T> T json(ACell o) {
6269
}
6370
return (T) list;
6471
}
65-
72+
6673
return (T) o.toString();
6774
}
6875

@@ -79,6 +86,21 @@ public static String jsonKey(ACell k) {
7986
return ((Keyword) k).getName().toString();
8087
return RT.toString(k);
8188
}
89+
90+
/**
91+
* Gets a String from a value suitable for use as a JSON map key
92+
*
93+
* @param o Value to convert to a JSON key
94+
* @return String usable as JSON key
95+
*/
96+
public static String jsonKey(Object o) {
97+
if (o instanceof ACell cell)
98+
return jsonKey(cell);
99+
100+
if (o instanceof String s) return s;
101+
102+
throw new IllegalArgumentException("Invalid yupe for JSON key: "+Utils.getClassName(o));
103+
}
82104

83105
/**
84106
* Converts a CVM Map to a JSON representation
@@ -98,30 +120,224 @@ public static HashMap<String, Object> jsonMap(AMap<?, ?> m) {
98120
}
99121
return hm;
100122
}
101-
123+
124+
/**
125+
* Convert any object to JSON
126+
*
127+
* @param value Value to convert to JSON, may be Java or CVM structure
128+
* @return JSON String
129+
*/
102130
public static String toString(Object value) {
103131
return toCVMString(value).toString();
104132
}
105133

106-
private static AString toCVMString(Object value) {
107-
BlobBuilder bb=new BlobBuilder();
108-
appendCVMString(bb,value);
134+
/**
135+
* Convert any object to JSON
136+
*
137+
* @param value Value to convert to JSON, may be Java or CVM structure
138+
* @return CVM String containing valid JSON
139+
*/
140+
public static AString toCVMString(Object value) {
141+
BlobBuilder bb = new BlobBuilder();
142+
appendCVMString(bb, value);
109143
return Strings.create(bb.toBlob());
110144
}
145+
146+
private static void appendCVMString(BlobBuilder bb, Object value) {
147+
if (value == null) {
148+
bb.append(Strings.NULL);
149+
return;
150+
}
151+
152+
if (value instanceof ACell cell) {
153+
appendCVMString(bb,cell);
154+
return;
155+
}
111156

112-
private static void appendCVMString(BlobBuilder bb,Object value) {
113-
if (value==null) bb.append(Strings.NULL);
114157

158+
if (value instanceof Map mv) {
159+
bb.append('{');
160+
int i=0;
161+
@SuppressWarnings("unchecked")
162+
Iterator<Map.Entry<Object,Object>> it = mv.entrySet().iterator();
163+
while (it.hasNext()) {
164+
Entry<Object, Object> me = it.next();
165+
if (i>0) bb.append("\n");
166+
appendCVMString(bb, jsonKey(me.getKey()));
167+
bb.append(':');
168+
bb.append(' ');
169+
appendCVMString(bb, me.getValue());
170+
171+
i += 1;
172+
}
173+
174+
bb.append('}');
175+
return;
176+
}
177+
178+
// This catches Java lists
115179
if (value instanceof List lv) {
116180
bb.append('[');
117-
int n=lv.size();
118-
for (int i=0; i<n; i++) {
119-
appendCVMString(bb,lv.get(i));
181+
int n = lv.size();
182+
for (int i = 0; i < n; i++) {
183+
if (i>0) bb.append(' ');
184+
appendCVMString(bb, lv.get(i));
185+
}
186+
bb.append(']');
187+
return;
188+
}
189+
190+
if (value instanceof Boolean bv) {
191+
bb.append(bv ? Strings.TRUE : Strings.FALSE);
192+
return;
193+
}
194+
195+
if (value instanceof String cs) {
196+
bb.append('\"');
197+
appendCVMStringQuoted(bb, cs);
198+
bb.append('\"');
199+
return;
200+
}
201+
202+
if (value instanceof Number nv) {
203+
if (value instanceof Double dv) {
204+
if (Double.isFinite(dv)) {
205+
bb.append(nv.toString());
206+
return;
207+
} else {
208+
if (Double.isNaN(dv)) {
209+
bb.append(JS_NAN);
210+
} else {
211+
if (dv<0) {
212+
bb.append('-');
213+
}
214+
bb.append("Infinity");
215+
}
216+
}
217+
return;
218+
}
219+
220+
bb.append(nv.toString());
221+
return;
222+
}
223+
224+
throw new IllegalArgumentException("Can't print type as JSON: "+Utils.getClassName(value));
225+
}
226+
227+
// Specialised writing for CVM types
228+
private static void appendCVMString(BlobBuilder bb, ACell value) {
229+
if (value == null) {
230+
bb.append(Strings.NULL);
231+
return;
232+
}
233+
234+
if (value instanceof AString cs) {
235+
bb.append('\"');
236+
appendCVMStringQuoted(bb, cs.toString()); // TODO: can be faster
237+
bb.append('\"');
238+
return;
239+
}
240+
241+
if (value instanceof ASymbolic cs) {
242+
// Print as the symbolic name string
243+
appendCVMString(bb, cs.getName());
244+
return;
245+
}
246+
247+
// CVM map special treatment
248+
if (value instanceof AMap mv) {
249+
bb.append('{');
250+
long n = mv.size();
251+
for (long i = 0; i < n; i++) {
252+
if (i>0) bb.append(' ');
253+
MapEntry<?,?> me=mv.entryAt(i);
254+
appendCVMString(bb, jsonKey(me.getKey()));
255+
bb.append(':');
120256
bb.append(' ');
257+
appendCVMString(bb, me.getValue());
258+
}
259+
bb.append('}');
260+
return;
261+
}
262+
263+
// Maps, Lists and Sets get printed as JSON arrays
264+
if (value instanceof ACollection lv) {
265+
bb.append('[');
266+
long n = lv.count();
267+
for (long i = 0; i < n; i++) {
268+
if (i>0) bb.append(' ');
269+
appendCVMString(bb, lv.get(i));
121270
}
122271
bb.append(']');
272+
return;
273+
}
274+
275+
276+
if (value instanceof CVMLong nv) {
277+
appendCVMString(bb,nv.longValue());
278+
return;
123279
}
124280

281+
if (value instanceof CVMDouble nv) {
282+
appendCVMString(bb,nv.doubleValue());
283+
return;
284+
}
285+
286+
if (value instanceof CVMBool bv) {
287+
bb.append(bv.booleanValue() ? Strings.TRUE : Strings.FALSE);
288+
return;
289+
}
290+
}
291+
292+
293+
private static void appendCVMStringQuoted(BlobBuilder bb, CharSequence cs) {
294+
int n = cs.length();
295+
for (int i = 0; i < n; i++) {
296+
char c = cs.charAt(i);
297+
AString rep = getReplacementString(c);
298+
if (rep != null) {
299+
bb.append(rep);
300+
} else {
301+
bb.append(c);
302+
}
303+
}
304+
}
305+
306+
private static final StringShort QUOTED_BACKSLASH = StringShort.create("\\\\");
307+
private static final StringShort QUOTED_QUOTES = StringShort.create("\\\"");
308+
private static final StringShort QUOTED_NEWLINE = StringShort.create("\\n");
309+
private static final StringShort QUOTED_RETURN = StringShort.create("\\r");
310+
private static final StringShort QUOTED_TAB = StringShort.create("\\t");
311+
312+
private static final StringShort JS_NAN = StringShort.create("NaN");
313+
314+
private static final char CONTROL_CHARS_END = 0x001f; // Highest ASCII control character
315+
316+
317+
private static AString getReplacementString(char c) {
318+
if (c == '\\') {
319+
return QUOTED_BACKSLASH;
320+
}
321+
if (c > '"') {
322+
// anything above this is OK in a JSON String
323+
return null;
324+
}
325+
if (c == '"') {
326+
return QUOTED_QUOTES;
327+
}
328+
if (c > CONTROL_CHARS_END) {
329+
return null;
330+
}
331+
if (c == '\n') {
332+
return QUOTED_NEWLINE;
333+
}
334+
if (c == '\r') {
335+
return QUOTED_RETURN;
336+
}
337+
if (c == '\t') {
338+
return QUOTED_TAB;
339+
}
340+
return StringShort.create(Blob.wrap(new byte[] { '\\', 'u', '0', '0', (byte) Utils.toHexChar((c >> 4) & 0x000f), (byte) Utils.toHexChar(c & 0x000f) }));
125341
}
126342

127343
}

convex-core/src/test/java/convex/core/data/StringsTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import convex.core.data.prim.CVMChar;
1414
import convex.core.data.util.BlobBuilder;
1515
import convex.core.lang.RT;
16+
import convex.core.util.JSONUtils;
1617
import convex.test.Samples;
1718

1819
public class StringsTest {
@@ -234,6 +235,9 @@ public void doStringTest(AString a) {
234235
AString abs=Strings.create(bs);
235236
assertEquals(a,abs);
236237

238+
// JSON print
239+
JSONUtils.toString(a);
240+
237241
// fall back to bloblike tests
238242
BlobsTest.doBlobLikeTests(a);
239243
}

convex-core/src/test/java/convex/core/lang/RTTest.java

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,20 @@
55
import static org.junit.jupiter.api.Assertions.assertSame;
66

77
import java.util.ArrayList;
8-
import java.util.HashMap;
9-
import java.util.List;
108

119
import org.junit.jupiter.api.Test;
1210

1311
import convex.core.cvm.Address;
1412
import convex.core.cvm.Keywords;
15-
import convex.core.cvm.Symbols;
16-
import convex.core.data.ACell;
1713
import convex.core.data.AList;
1814
import convex.core.data.AVector;
19-
import convex.core.data.Blob;
20-
import convex.core.data.Blobs;
21-
import convex.core.data.Index;
2215
import convex.core.data.Keyword;
2316
import convex.core.data.Lists;
24-
import convex.core.data.Maps;
2517
import convex.core.data.Strings;
2618
import convex.core.data.Symbol;
2719
import convex.core.data.Vectors;
28-
import convex.core.data.prim.CVMBool;
29-
import convex.core.data.prim.CVMChar;
3020
import convex.core.data.prim.CVMDouble;
3121
import convex.core.data.prim.CVMLong;
32-
import convex.core.util.JSONUtils;
3322

3423
/**
3524
* Tests for RT functions.
@@ -78,61 +67,7 @@ public void testSequence() {
7867
assertNull(RT.sequence(Keywords.FOO)); // keywords not allowed
7968
}
8069

81-
@Test
82-
public void testJSON() {
83-
assertNull(JSONUtils.json(null));
84-
85-
assertEquals((Long)13L,JSONUtils.json(Address.create(13)));
86-
assertEquals("0xcafebabe",JSONUtils.json(Blob.fromHex("cafebabe")));
87-
assertEquals("0x",JSONUtils.json(Blobs.empty()));
88-
assertEquals("{}",JSONUtils.json(Index.none()).toString());
89-
assertEquals("{}",JSONUtils.json(Maps.empty()).toString());
90-
assertEquals("[1, 2]",JSONUtils.json(Vectors.of(1,2)).toString());
91-
assertEquals("[1, 2]",JSONUtils.json(Lists.of(1,2)).toString());
92-
assertEquals("c",JSONUtils.json(CVMChar.create('c')));
93-
94-
assertEquals("foo",JSONUtils.json(Symbols.FOO));
95-
assertEquals(":foo",JSONUtils.json(Keywords.FOO));
96-
97-
// Note keywords get colon removed when used as JSON key
98-
assertEquals(":bar",JSONUtils.jsonMap(Maps.of(Keywords.FOO, Keywords.BAR)).get("foo"));
99-
100-
101-
// JSON should convert keys to strings
102-
assertEquals(Maps.of("1",2), RT.cvm(JSONUtils.json(Maps.of(1,2))));
103-
assertEquals(Maps.of("[]",3), RT.cvm(JSONUtils.json(Maps.of(Vectors.empty(),3))));
104-
assertEquals(Maps.of("[\"\" 3]",4), RT.cvm(JSONUtils.json(Maps.of(Vectors.of("",3),4))));
105-
}
106-
107-
@Test
108-
public void testJSONRoundTrips() {
109-
110-
doJSONRoundTrip(1L,CVMLong.ONE);
111-
doJSONRoundTrip(1.0,CVMDouble.ONE);
112-
doJSONRoundTrip(null,null);
113-
114-
doJSONRoundTrip(new ArrayList<Object>(),Vectors.empty());
115-
doJSONRoundTrip(List.of(1,2),Vectors.of(1,2));
116-
doJSONRoundTrip("hello",Strings.create("hello"));
117-
doJSONRoundTrip("",Strings.EMPTY);
118-
doJSONRoundTrip(true,CVMBool.TRUE);
119-
120-
doJSONRoundTrip(new HashMap<String,Object>(),Maps.empty());
121-
doJSONRoundTrip(Maps.hashMapOf("1",2,"3",4),Maps.of("1",2,"3",4));
122-
}
12370

124-
private void doJSONRoundTrip(Object o, ACell c) {
125-
// o should convert to c
126-
assertEquals(c,RT.cvm(o));
127-
128-
// c should round trip via JSON back to c, since JSON is a subset of CVM types
129-
ACell roundTrip=RT.cvm(JSONUtils.json(c));
130-
assertEquals(c,roundTrip);
131-
132-
// c should also round trip via JVM equivalent, since we are using JSON subset
133-
ACell roundTrip2=RT.cvm(RT.jvm(c));
134-
assertEquals(c,roundTrip2);
135-
}
13671

13772
@Test
13873
public void testVec() {

0 commit comments

Comments
 (0)