Skip to content

Commit

Permalink
Merge pull request #9 from rhysdh540/master
Browse files Browse the repository at this point in the history
improvements
  • Loading branch information
Nolij authored Jul 12, 2024
2 parents ae3155d + ff04d83 commit 71e4356
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 33 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ build/
.idea/
!gradle/wrapper/gradle-wrapper.jar
!**/src/**/build/
*.json5
.DS_Store
12 changes: 12 additions & 0 deletions spec.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
// comments
unquoted: 'and you can quote me on that',
singleQuotes: 'I can use "double quotes" here',
lineBreaks: "Look, Mom! \
No \\n's!",
hexadecimal: 0xdecaf,
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
positiveSign: +1,
trailingComma: 'in objects', andIn: ['arrays',],
"backwardsCompatible": "with JSON",
}
142 changes: 111 additions & 31 deletions src/main/java/dev/nolij/zson/Zson.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.nolij.zson;

import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -10,15 +11,23 @@
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

import java.nio.file.Files;
import java.nio.file.Path;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.Map.Entry;

@SuppressWarnings({"deprecation", "UnstableApiUsage", "unused"})
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@SuppressWarnings({"deprecation", "UnstableApiUsage"})
public final class Zson {
//region Helper Methods

Expand All @@ -27,7 +36,7 @@ public final class Zson {
*/
@NotNull
@Contract("_, _, _ -> new")
public static Map.Entry<String, ZsonValue> entry(@Nullable String key, @Nullable String comment, @Nullable Object value) {
public static Map.Entry<String, ZsonValue> entry(@NotNull String key, @Nullable String comment, @Nullable Object value) {
return new AbstractMap.SimpleEntry<>(key, new ZsonValue(comment, value));
}

Expand All @@ -36,7 +45,7 @@ public static Map.Entry<String, ZsonValue> entry(@Nullable String key, @Nullable
*/
@NotNull
@Contract(value = "_, _ -> new", pure = true)
public static Map.Entry<String, ZsonValue> entry(@Nullable String key, @Nullable Object value) {
public static Map.Entry<String, ZsonValue> entry(@NotNull String key, @Nullable Object value) {
return new AbstractMap.SimpleEntry<>(key, new ZsonValue(value));
}

Expand All @@ -50,7 +59,7 @@ public static Map.Entry<String, ZsonValue> entry(@Nullable String key, @Nullable
@Contract("_ -> new")
public static Map<String, ZsonValue> object(@NotNull Map.Entry<String, ZsonValue>... entries) {
Map<String, ZsonValue> map = new LinkedHashMap<>();
for (Entry<String, ZsonValue> e : entries) {
for (Map.Entry<String, ZsonValue> e : entries) {
map.put(e.getKey(), e.getValue());
}
return map;
Expand All @@ -70,6 +79,16 @@ public static List<?> array(@NotNull Object... values) {
return list;
}

public static <E extends Enum<E>> void convertEnum(Map<String, ZsonValue> json, String key, Class<E> enumClass) {
ZsonValue value = json.get(key);
if(value == null) return;
if(value.value instanceof String s) {
json.put(key, new ZsonValue(value.comment, Enum.valueOf(enumClass, s)));
} else if(!enumClass.isInstance(value.value)) {
throw new IllegalArgumentException("Expected string, got " + value.value);
}
}

/**
* "Un-escapes" a string by replacing escape sequences with their actual characters.
* @param string the string to un-escape. May be null.
Expand Down Expand Up @@ -208,7 +227,7 @@ public static String escape(@Nullable String string, char escapeQuotes) {
@Contract("_ -> new")
public static Map<String, ZsonValue> obj2Map(@Nullable Object object) {
if(object == null) return object();
Map<String, ZsonValue> map = Zson.object();
Map<String, ZsonValue> map = object();
for (Field field : object.getClass().getDeclaredFields()) {
if(!shouldInclude(field, true)) continue;
ZsonField value = field.getAnnotation(ZsonField.class);
Expand Down Expand Up @@ -298,11 +317,11 @@ private static <T> void setField(Field field, Object object, Object value) {
case "char" -> field.setChar(object, (char) value);
}
} else {
if(type.isEnum()) {
field.set(object, Enum.valueOf((Class<Enum>) type, (String) value));
} else {
field.set(object, type.cast(value));
Object finalValue = value;
if (type.isEnum() && value instanceof String) {
finalValue = Enum.valueOf((Class<Enum>) type, (String) value);
}
field.set(object, finalValue);
}
} catch (Exception e) {
throw new AssertionError(
Expand Down Expand Up @@ -339,7 +358,7 @@ public static <T> T parseFile(@NotNull Path path) throws IOException {
*/
@Nullable
@Contract(pure = true)
public static <T> T parseString(@NotNull String serialized) {
public static <T> T parseString(@NotNull @Language("json5") String serialized) {
try {
return parse(new StringReader(serialized));
} catch (IOException e) {
Expand Down Expand Up @@ -385,9 +404,9 @@ public static <T> T parse(Reader input) throws IOException {
return (T) parseArray(input);
}
case '"', '\'' -> {
return (T) Zson.unescape(parseString(input, (char) ch));
return (T) unescape(parseString(input, (char) ch));
}
case '-', '+', 'N', 'I',
case '.', '-', '+', 'N', 'I',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
return (T) parseNumber(input, (char) ch);
}
Expand Down Expand Up @@ -416,7 +435,7 @@ public static <T> T parse(Reader input) throws IOException {
*/
@Contract(mutates = "param")
private static Map<String, ZsonValue> parseObject(Reader input) throws IOException {
var map = Zson.object();
var map = object();

var comma = false;
var colon = false;
Expand Down Expand Up @@ -453,10 +472,10 @@ private static Map<String, ZsonValue> parseObject(Reader input) throws IOExcepti

if (key == null) {
key = switch (ch) {
case '"', '\'' -> Zson.unescape(parseString(input, (char) ch));
case '"', '\'' -> unescape(parseString(input, (char) ch));
default -> {
if (Character.isLetter(ch) || ch == '_' || ch == '$' || ch == '\\') {
yield parseIdentifier(input, (char) ch);
if (Character.isJavaIdentifierStart(ch) || ch == '\\') {
yield parseIdentifier(input, ch);
} else {
throw unexpected(ch);
}
Expand Down Expand Up @@ -548,7 +567,7 @@ private static String parseString(Reader input, char start) throws IOException {
if (c == '\\') {
escapes++;
if (escapes == 2) {
output.append('\\');
output.append("\\\\");
escapes = 0;
}
} else {
Expand All @@ -569,16 +588,29 @@ private static String parseString(Reader input, char start) throws IOException {
* @return The parsed identifier
* @throws IOException If an I/O error occurs
*/
// TODO: handle multi-character escapes
@Contract(mutates = "param1")
private static String parseIdentifier(Reader input, char start) throws IOException {
private static String parseIdentifier(Reader input, int start) throws IOException {
var output = new StringBuilder();
output.append(start);
boolean escaped = start == '\\';

if(!escaped)
output.append((char) start);

int c;
input.mark(1);
while ((c = input.read()) != -1) {
// TODO: verify this works properly... https://262.ecma-international.org/5.1/#sec-7.6
if (!Character.isWhitespace(c)) {
if(escaped) {
if(c == 'n' || c == 'r') {
throw unexpected(c);
}
output.append(unescape("\\" + (char) c));
input.mark(1);
escaped = false;
} else if (c == '\\') {
input.mark(1);
escaped = true;
} else if (isIdentifierChar(c)) {
input.mark(1);
output.append(Character.toChars(c));
} else {
Expand All @@ -590,6 +622,35 @@ private static String parseIdentifier(Reader input, char start) throws IOExcepti
throw unexpectedEOF();
}

/**
* Checks if the given character is a valid EMCAScript <i>IdentifierName</i> character.
* This is true if the character is an underscore ({@code _}), a dollar sign ({@code $}),
* or a character in one of the following Unicode categories:
* <ul>
* <li>Uppercase letter (Lu)</li>
* <li>Lowercase letter (Ll)</li>
* <li>Titlecase letter (Lt)</li>
* <li>Modifier letter (Lm)</li>
* <li>Other letter (Lo)</li>
* <li>Letter Number (Nl)</li>
* <li>Non-spacing mark (Mn)</li>
* <li>Combining spacing mark (Mc)</li>
* <li>Decimal number (Nd)</li>
* <li>Connector punctuation (Pc)</li>
* </ul>
* @param c the code point to check
* @return {@code true} if the character is a valid identifier character, {@code false} otherwise
* @see <a href="https://262.ecma-international.org/5.1/#sec-7.6">ECMAScript 5.1 §7.6</a>
*/
private static boolean isIdentifierChar(int c) {
if(c == '_' || c == '$') return true;
int type = Character.getType(c);
return type == Character.UPPERCASE_LETTER || type == Character.LOWERCASE_LETTER || type == Character.TITLECASE_LETTER ||
type == Character.MODIFIER_LETTER || type == Character.OTHER_LETTER || type == Character.LETTER_NUMBER ||
type == Character.NON_SPACING_MARK || type == Character.COMBINING_SPACING_MARK || type == Character.DECIMAL_DIGIT_NUMBER ||
type == Character.CONNECTOR_PUNCTUATION;
}

/**
* Parses a JSON boolean from the given {@link Reader}. The reader should be positioned at the start of the boolean.
* @param input The reader to parse the boolean from
Expand Down Expand Up @@ -689,6 +750,10 @@ private static Number parseNumber(Reader input, char start) throws IOException {
return parseDecimal(input, '0');
}
}

case '.' -> {
return parseDecimal(input, '.');
}
}

throw unexpected(start);
Expand Down Expand Up @@ -778,9 +843,7 @@ private static boolean skipComment(Reader input) throws IOException {
if (c == '/') {
int c2 = input.read();
if (c2 == '/') {
while ((c = input.read()) != -1)
if (c == '\n')
break;
while ((c = input.read()) != -1 && c != '\n');

return true;
} else if (c2 == '*') {
Expand Down Expand Up @@ -864,7 +927,6 @@ public void write(@NotNull Map<String, ZsonValue> data, @NotNull Appendable outp
output.append("{\n");

for (var entry : data.entrySet()) {
String key = entry.getKey();
ZsonValue zv = entry.getValue();
String comment = zv.comment;

Expand All @@ -882,7 +944,7 @@ public void write(@NotNull Map<String, ZsonValue> data, @NotNull Appendable outp
if (quoteKeys)
output.append('"');

output.append(key);
output.append(checkIdentifier(entry.getKey()));
if (quoteKeys)
output.append('"');

Expand All @@ -892,6 +954,24 @@ public void write(@NotNull Map<String, ZsonValue> data, @NotNull Appendable outp
output.append("}");
}

/**
* Checks if the given string is a valid identifier.
* @param key The string to check.
* @return {@code true} if the string is a valid identifier, {@code false} otherwise.
* @see #isIdentifierChar(int)
* @see <a href="https://262.ecma-international.org/5.1/#sec-7.6">ECMAScript 5.1 §7.6</a>
*/
private String checkIdentifier(String key) {
if(key == null || key.isEmpty()) throw new IllegalArgumentException("Key cannot be null or empty");
int c = key.charAt(0);
if(!Character.isJavaIdentifierStart(c) && c != '\\') throw new IllegalArgumentException("Key must start with a valid identifier character: " + key.charAt(0));
for (int i = 1; i < key.length(); i++) {
if(!isIdentifierChar(key.charAt(i))) throw new IllegalArgumentException("Key must be a valid Java identifier: " + key);
}

return key;
}

/**
* Converts the given object to a JSON5 value.
* @param value The value to convert.
Expand All @@ -914,7 +994,7 @@ private String value(Object value) {
throw new StackOverflowError("Map is circular");
}
} else if (value instanceof String stringValue) {
return '"' + Zson.escape(stringValue, '"') + '"';
return '"' + escape(stringValue, '"') + '"';
} else if (value instanceof Number || value instanceof Boolean || value == null) {
return String.valueOf(value);
} else if (value instanceof Iterable<?> iterableValue) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/dev/nolij/zson/ZsonValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public ZsonValue(Object value) {

@Override
public int hashCode() {
return value == null ? 0 : value.hashCode();
return Objects.hashCode(value);
}

@Override
Expand Down
Loading

0 comments on commit 71e4356

Please sign in to comment.