Skip to content

Commit 28d8358

Browse files
Merge pull request #29 from felipestanzani/conformance
Conformance test suite implemented successfully.
2 parents 981cded + 6c6dee3 commit 28d8358

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4500
-282
lines changed

.github/dependabot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ updates:
33
- package-ecosystem: "gradle"
44
directory: "/"
55
schedule:
6-
interval: "weekly"
6+
interval: "monthly"
77
open-pull-requests-limit: 10
88
groups:
99
test:
@@ -13,4 +13,4 @@ updates:
1313
- package-ecosystem: "github-actions"
1414
directory: "/"
1515
schedule:
16-
interval: "weekly"
16+
interval: "monthly"

.idea/misc.xml

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

.idea/sonarlint.xml

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

sonar-project.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sonar.java.source=17

src/main/java/com/felipestanzani/jtoon/DecodeOptions.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,46 @@
1010
* IllegalArgumentException on invalid input. When false,
1111
* uses best-effort parsing and returns null on errors
1212
* (default: true)
13+
* @param expandPaths Path expansion mode for dotted keys (default: OFF)
1314
*/
1415
public record DecodeOptions(
1516
int indent,
1617
Delimiter delimiter,
17-
boolean strict) {
18+
boolean strict,
19+
PathExpansion expandPaths) {
1820
/**
19-
* Default decoding options: 2 spaces indent, comma delimiter, strict validation
21+
* Default decoding options: 2 spaces indent, comma delimiter, strict validation, path expansion off
2022
*/
21-
public static final DecodeOptions DEFAULT = new DecodeOptions(2, Delimiter.COMMA, true);
23+
public static final DecodeOptions DEFAULT = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF);
2224

2325
/**
2426
* Creates DecodeOptions with default values.
2527
*/
2628
public DecodeOptions() {
27-
this(2, Delimiter.COMMA, true);
29+
this(2, Delimiter.COMMA, true, PathExpansion.OFF);
2830
}
2931

3032
/**
3133
* Creates DecodeOptions with custom indent, using default delimiter and strict
3234
* mode.
3335
*/
3436
public static DecodeOptions withIndent(int indent) {
35-
return new DecodeOptions(indent, Delimiter.COMMA, true);
37+
return new DecodeOptions(indent, Delimiter.COMMA, true, PathExpansion.OFF);
3638
}
3739

3840
/**
3941
* Creates DecodeOptions with custom delimiter, using default indent and strict
4042
* mode.
4143
*/
4244
public static DecodeOptions withDelimiter(Delimiter delimiter) {
43-
return new DecodeOptions(2, delimiter, true);
45+
return new DecodeOptions(2, delimiter, true, PathExpansion.OFF);
4446
}
4547

4648
/**
4749
* Creates DecodeOptions with custom strict mode, using default indent and
4850
* delimiter.
4951
*/
5052
public static DecodeOptions withStrict(boolean strict) {
51-
return new DecodeOptions(2, Delimiter.COMMA, strict);
53+
return new DecodeOptions(2, Delimiter.COMMA, strict, PathExpansion.OFF);
5254
}
5355
}

src/main/java/com/felipestanzani/jtoon/JToon.java

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,9 @@
44
import com.felipestanzani.jtoon.encoder.ValueEncoder;
55
import com.felipestanzani.jtoon.normalizer.JsonNormalizer;
66
import tools.jackson.databind.JsonNode;
7-
import tools.jackson.databind.ObjectMapper;
87

9-
/**
10-
* Main API for encoding and decoding JToon format.
11-
*
12-
* <p>
13-
* JToon is a structured text format that represents JSON-like data in a more
14-
* human-readable way, with support for tabular arrays and inline formatting.
15-
* </p>
16-
*
17-
* <h2>Usage Examples:</h2>
18-
*
19-
* <pre>{@code
20-
* // Encode a Java object with default options
21-
* String toon = JToon.encode(myObject);
22-
*
23-
* // Encode with custom options
24-
* EncodeOptions options = new EncodeOptions(4, Delimiter.PIPE, true);
25-
* String toon = JToon.encode(myObject, options);
26-
*
27-
* // Encode a plain JSON string directly
28-
* String toon = JToon.encodeJson("{\"id\":123,\"name\":\"Ada\"}");
29-
*
30-
* // Decode TOON back to Java objects
31-
* Object result = JToon.decode(toon);
32-
*
33-
* // Decode TOON directly to JSON string
34-
* String json = JToon.decodeToJson(toon);
35-
* }</pre>
36-
*/
378
public final class JToon {
389

39-
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
40-
4110
private JToon() {
4211
throw new UnsupportedOperationException("Utility class cannot be instantiated");
4312
}
@@ -179,11 +148,6 @@ public static String decodeToJson(String toon) {
179148
* invalid
180149
*/
181150
public static String decodeToJson(String toon, DecodeOptions options) {
182-
try {
183-
Object decoded = ValueDecoder.decode(toon, options);
184-
return OBJECT_MAPPER.writeValueAsString(decoded);
185-
} catch (Exception e) {
186-
throw new IllegalArgumentException("Failed to convert decoded value to JSON: " + e.getMessage(), e);
187-
}
151+
return ValueDecoder.decodeToJson(toon, options);
188152
}
189153
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.felipestanzani.jtoon;
2+
3+
/**
4+
* Path expansion mode for decoding dotted keys.
5+
*/
6+
public enum PathExpansion {
7+
/**
8+
* Safe mode: expands dotted keys like "a.b.c" into nested objects.
9+
* Only expands keys that are valid identifier segments.
10+
*/
11+
SAFE,
12+
13+
/**
14+
* Off mode: treats dotted keys as literal keys.
15+
*/
16+
OFF
17+
}
18+

src/main/java/com/felipestanzani/jtoon/decoder/PrimitiveDecoder.java

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
* Converts TOON scalar representations to appropriate Java types:
1010
* </p>
1111
* <ul>
12-
* <li>{@code "null"} → {@code null}</li>
13-
* <li>{@code "true"} / {@code "false"} → {@code Boolean}</li>
14-
* <li>Numeric strings → {@code Long} or {@code Double}</li>
15-
* <li>Quoted strings → {@code String} (with unescaping)</li>
16-
* <li>Bare strings → {@code String}</li>
12+
* <li>{@code "null"} → {@code null}</li>
13+
* <li>{@code "true"} / {@code "false"} → {@code Boolean}</li>
14+
* <li>Numeric strings → {@code Long} or {@code Double}</li>
15+
* <li>Quoted strings → {@code String} (with unescaping)</li>
16+
* <li>Bare strings → {@code String}</li>
1717
* </ul>
1818
*
1919
* <h2>Examples:</h2>
20+
*
2021
* <pre>{@code
2122
* parse("null") → null
2223
* parse("true") → true
@@ -46,27 +47,52 @@ static Object parse(String value) {
4647
}
4748

4849
// Check for null literal
49-
if ("null".equals(value)) {
50-
return null;
51-
}
52-
53-
// Check for boolean literals
54-
if ("true".equals(value)) {
55-
return true;
56-
}
57-
if ("false".equals(value)) {
58-
return false;
50+
switch (value) {
51+
case "null" -> {
52+
return null;
53+
}
54+
case "true" -> {
55+
return true;
56+
}
57+
case "false" -> {
58+
return false;
59+
}
60+
default -> {
61+
// Do nothing, continue to next check
62+
}
5963
}
6064

6165
// Check for quoted strings
62-
if (value.startsWith("\"") && value.endsWith("\"")) {
66+
if (value.startsWith("\"")) {
67+
// Validate string before unescaping
68+
StringEscaper.validateString(value);
6369
return StringEscaper.unescape(value);
6470
}
6571

72+
// Check for leading zeros (treat as string, except for "0", "-0", "0.0", etc.)
73+
String trimmed = value.trim();
74+
if (trimmed.length() > 1 && trimmed.matches("^-?0+[\\d].*")
75+
&& !trimmed.matches("^-?0+(\\.0+)?([eE][+-]?\\d+)?$")) {
76+
return value;
77+
}
78+
6679
// Try parsing as number
6780
try {
81+
// Check if it contains exponent notation or decimal point
6882
if (value.contains(".") || value.contains("e") || value.contains("E")) {
69-
return Double.parseDouble(value);
83+
double parsed = Double.parseDouble(value);
84+
// Handle negative zero - Java doesn't distinguish, but spec says it should be 0
85+
if (parsed == 0.0) {
86+
return 0L;
87+
}
88+
// Check if the result is a whole number - if so, return as Long
89+
if (parsed == Math.floor(parsed)
90+
&& !Double.isInfinite(parsed)
91+
&& parsed >= Long.MIN_VALUE
92+
&& parsed <= Long.MAX_VALUE)
93+
return (long) parsed;
94+
95+
return parsed;
7096
} else {
7197
return Long.parseLong(value);
7298
}

0 commit comments

Comments
 (0)