Skip to content

Commit 524d09d

Browse files
committed
Release 0.3.1 — sync to ktav 0.3.1 + spec 0.1.1
Backward-compatible feature release tracking ktav 0.3.1 and spec 0.1.1. Added - Top-level Array support (spec 0.1.1) — Ktav.loads now returns Value.Arr when the document begins with array-item shapes; Ktav.dumps accepts Value.Arr at the top level. - Ktav.toStringForceStrings(Value) — render with every leaf scalar coerced to a String via the raw marker (::). Changed - Picked up ktav 0.3.1 (workspace dep + cabi). - spec submodule synced to 7256816 (spec 0.1.1). - NativeLoader.LIB_VERSION bumped 0.1.2 -> 0.3.1 to track the matching GitHub Release asset (cabi adds ktav_dumps_force_strings).
1 parent 31e2efe commit 524d09d

10 files changed

Lines changed: 238 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@ This changelog tracks **binding releases**, not changes to the Ktav format
1111
itself — for the latter see
1212
[`ktav-lang/spec`](https://github.com/ktav-lang/spec/blob/main/CHANGELOG.md).
1313

14+
## 0.3.1 — 2026-05-10
15+
16+
### Added
17+
18+
- **Top-level Array support** (spec § 5.0.1, ktav 0.3.1) — first
19+
content line decides Object vs Array. `Ktav.loads` now returns
20+
`Value.Arr` when the document begins with array-item shapes (bare
21+
scalars, typed/raw markers, lone openers, multi-line openers); all
22+
prior 0.3.0-valid documents still parse to the same `Value`.
23+
`Ktav.dumps` accepts a `Value.Arr` at the top level as well.
24+
- **`Ktav.toStringForceStrings(Value)`** — render any Value with every
25+
leaf scalar (typed integers `:i`, typed floats `:f`, booleans, and
26+
`null`) coerced to a String via the raw marker (`::`). Compounds
27+
preserve their structure. Useful for "everything is a string"
28+
dumps. Round-trips back through `loads` as the same set of String
29+
scalars.
30+
31+
### Changed
32+
33+
- **Picked up `ktav 0.3.1`** — tracks ktav 0.3.1 (additive). Spec
34+
submodule synced to `7256816` (spec 0.1.1).
35+
36+
### Native binary cache
37+
38+
- `NativeLoader.LIB_VERSION` bumped to `0.3.1`. Fresh download from the
39+
matching GitHub Release on first call after upgrade.
40+
41+
1442
## 0.3.0 — 2026-05-08
1543

1644
### Changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ resolver = "2"
33
members = ["crates/cabi"]
44

55
[workspace.package]
6-
version = "0.3.0"
6+
version = "0.3.1"
77
edition = "2021"
88
license = "MIT"
99
repository = "https://github.com/ktav-lang/java"
1010
rust-version = "1.70"
1111

1212
[workspace.dependencies]
13-
ktav = "0.3.0"
13+
ktav = "0.3.1"
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = { version = "1", features = ["preserve_order"] }
1616
indexmap = "2"

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44

55
allprojects {
66
group = "io.github.ktav-lang"
7-
version = "0.3.0"
7+
version = "0.3.1"
88
}
99

1010
subprojects {

crates/cabi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ rust-version.workspace = true
99
publish = false
1010

1111
[package.metadata.ktav]
12-
spec-version = "0.1.0"
12+
spec-version = "0.1.1"
1313

1414
[lib]
1515
name = "ktav_cabi"

crates/cabi/src/lib.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@
2222
//!
2323
//! ## C ABI
2424
//!
25-
//! Four functions, all use the same "caller-owned pointer, callee-owned
25+
//! Five functions, all use the same "caller-owned pointer, callee-owned
2626
//! buffer" pattern:
2727
//!
2828
//! - `ktav_loads(src, src_len, out_buf, out_len, out_err) -> i32`
2929
//! - `ktav_dumps(src, src_len, out_buf, out_len, out_err) -> i32`
30+
//! - `ktav_dumps_force_strings(src, src_len, out_buf, out_len, out_err) -> i32`
3031
//! - `ktav_free(ptr, len)` — free a buffer returned by loads/dumps.
3132
//! - `ktav_version()` — NUL-terminated static string, for sanity checks.
3233
//!
@@ -153,9 +154,9 @@ pub unsafe extern "C" fn ktav_dumps(
153154
}
154155
};
155156

156-
if !matches!(value, Value::Object(_)) {
157+
if !matches!(value, Value::Object(_) | Value::Array(_)) {
157158
emit_err(
158-
"top-level Ktav document must be an object".to_string(),
159+
"top-level Ktav document must be an object or array".to_string(),
159160
out_err,
160161
out_err_len,
161162
);
@@ -174,6 +175,65 @@ pub unsafe extern "C" fn ktav_dumps(
174175
0
175176
}
176177

178+
/// Render a JSON document to Ktav text, coercing every leaf scalar to a
179+
/// String (typed integers, typed floats, booleans, and `null` are
180+
/// flattened to their textual form via the `::` raw marker). Compounds
181+
/// preserve their structure. See `ktav::to_string_force_strings`.
182+
///
183+
/// # Safety
184+
/// Same as [`ktav_loads`].
185+
#[no_mangle]
186+
pub unsafe extern "C" fn ktav_dumps_force_strings(
187+
src: *const u8,
188+
src_len: usize,
189+
out_buf: *mut *mut u8,
190+
out_len: *mut usize,
191+
out_err: *mut *mut c_char,
192+
out_err_len: *mut usize,
193+
) -> c_int {
194+
*out_buf = ptr::null_mut();
195+
*out_len = 0;
196+
*out_err = ptr::null_mut();
197+
*out_err_len = 0;
198+
199+
let bytes = slice::from_raw_parts(src, src_len);
200+
let wire: WireValue = match serde_json::from_slice(bytes) {
201+
Ok(w) => w,
202+
Err(e) => {
203+
emit_err(format!("input JSON: {e}"), out_err, out_err_len);
204+
return 1;
205+
}
206+
};
207+
208+
let value = match wire.into_value() {
209+
Ok(v) => v,
210+
Err(e) => {
211+
emit_err(e, out_err, out_err_len);
212+
return 1;
213+
}
214+
};
215+
216+
if !matches!(value, Value::Object(_) | Value::Array(_)) {
217+
emit_err(
218+
"top-level Ktav document must be an object or array".to_string(),
219+
out_err,
220+
out_err_len,
221+
);
222+
return 1;
223+
}
224+
225+
let text = match ktav::to_string_force_strings(&value) {
226+
Ok(s) => s,
227+
Err(e) => {
228+
emit_err(e.to_string(), out_err, out_err_len);
229+
return 1;
230+
}
231+
};
232+
233+
emit(text.into_bytes(), out_buf, out_len);
234+
0
235+
}
236+
177237
/// Free a buffer returned by `ktav_loads` / `ktav_dumps` (success or
178238
/// error). `ptr`/`len` is a no-op when null/zero.
179239
///

lib/src/main/java/lang/ktav/Ktav.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ public static Value loads(String src) {
4545

4646
/**
4747
* Render a {@link Value} back to Ktav text. The top-level value must
48-
* be a {@link Value.Obj} — other shapes are rejected by the native
49-
* side. Throws {@link KtavException} on render error.
48+
* be a {@link Value.Obj} or {@link Value.Arr} — other shapes are
49+
* rejected by the native side. Top-level arrays are supported as of
50+
* spec 0.1.1 (binding 0.3.1). Throws {@link KtavException} on render
51+
* error.
5052
*/
5153
public static String dumps(Value value) {
5254
if (value == null) {
@@ -57,6 +59,33 @@ public static String dumps(Value value) {
5759
return new String(output, StandardCharsets.UTF_8);
5860
}
5961

62+
/**
63+
* Render a {@link Value} back to Ktav text with every leaf scalar
64+
* coerced to a String. Typed integers ({@code :i}), typed floats
65+
* ({@code :f}), booleans, and {@code null} are flattened to their
66+
* textual form via the raw-marker ({@code ::}). Compounds (objects
67+
* and arrays) preserve their structure; only leaf scalars are
68+
* coerced. The output round-trips back through {@link #loads} as the
69+
* same set of String scalars.
70+
*
71+
* <p>Useful for "everything is a string" dumps — e.g. for downstream
72+
* consumers that don't understand typed markers, or for diff-friendly
73+
* canonical text.
74+
*
75+
* <p>The top-level value must be a {@link Value.Obj} or
76+
* {@link Value.Arr}. Throws {@link KtavException} on render error.
77+
*
78+
* @since 0.3.1
79+
*/
80+
public static String toStringForceStrings(Value value) {
81+
if (value == null) {
82+
throw new NullPointerException("value");
83+
}
84+
byte[] input = WireJson.encode(value);
85+
byte[] output = callNative(NativeOp.DUMPS_FORCE_STRINGS, input);
86+
return new String(output, StandardCharsets.UTF_8);
87+
}
88+
6089
/**
6190
* Version of the loaded {@code ktav_cabi} native library. Useful for
6291
* sanity checks.
@@ -68,7 +97,8 @@ public static String nativeVersion() {
6897

6998
private enum NativeOp {
7099
LOADS,
71-
DUMPS
100+
DUMPS,
101+
DUMPS_FORCE_STRINGS
72102
}
73103

74104
private static byte[] callNative(NativeOp op, byte[] input) {
@@ -96,6 +126,8 @@ private static byte[] callNative(NativeOp op, byte[] input) {
96126
outBuf, outLen, outErr, outErrLen);
97127
case DUMPS -> lib.ktav_dumps(srcPtr, input.length,
98128
outBuf, outLen, outErr, outErrLen);
129+
case DUMPS_FORCE_STRINGS -> lib.ktav_dumps_force_strings(srcPtr, input.length,
130+
outBuf, outLen, outErr, outErrLen);
99131
};
100132

101133
if (rc != 0) {

lib/src/main/java/lang/ktav/internal/NativeLib.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ int ktav_dumps(
3232
PointerByReference outErr,
3333
LongByReference outErrLen);
3434

35+
int ktav_dumps_force_strings(
36+
Pointer src,
37+
long srcLen,
38+
PointerByReference outBuf,
39+
LongByReference outLen,
40+
PointerByReference outErr,
41+
LongByReference outErrLen);
42+
3543
void ktav_free(Pointer ptr, long len);
3644

3745
String ktav_version();

lib/src/main/java/lang/ktav/internal/NativeLoader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
public final class NativeLoader {
3232

3333
/** Version of {@code ktav_cabi} this build expects. Bump per release. */
34-
static final String LIB_VERSION = "0.1.2";
34+
static final String LIB_VERSION = "0.3.1";
3535

3636
private static final String RELEASE_BASE =
3737
"https://github.com/ktav-lang/java/releases/download/v";

lib/src/test/java/lang/ktav/SmokeTest.java

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,104 @@ void parseErrorThrows() {
102102
}
103103

104104
@Test
105-
void dumpsRejectsNonObjectTopLevel() {
106-
Value arr = new Value.Arr(List.of(new Value.Str("x")));
107-
assertThrows(KtavException.class, () -> Ktav.dumps(arr));
105+
void dumpsRejectsScalarTopLevel() {
106+
// Top-level Array is now valid (spec 0.1.1) — only bare scalars
107+
// are still rejected by the native side.
108+
assertThrows(KtavException.class, () -> Ktav.dumps(new Value.Str("just a string")));
109+
}
110+
111+
@Test
112+
void loadsTopLevelArrayBareScalars() {
113+
// spec 0.1.1: first content line decides Object vs Array.
114+
String src = """
115+
alpha
116+
beta
117+
gamma
118+
""";
119+
Value v = Ktav.loads(src);
120+
Value.Arr arr = assertInstanceOf(Value.Arr.class, v);
121+
assertEquals(List.of(
122+
new Value.Str("alpha"),
123+
new Value.Str("beta"),
124+
new Value.Str("gamma")), arr.items());
125+
}
126+
127+
@Test
128+
void loadsTopLevelArrayTypedItems() {
129+
String src = """
130+
:i 1
131+
:i 2
132+
:f 3.5
133+
""";
134+
Value v = Ktav.loads(src);
135+
Value.Arr arr = assertInstanceOf(Value.Arr.class, v);
136+
assertEquals(3, arr.items().size());
137+
assertEquals(new Value.Int("1"), arr.items().get(0));
138+
assertEquals(new Value.Int("2"), arr.items().get(1));
139+
Value.Flt third = assertInstanceOf(Value.Flt.class, arr.items().get(2));
140+
assertEquals(3.5, third.toDouble());
141+
}
142+
143+
@Test
144+
void roundTripTopLevelArray() {
145+
Value.Arr arr = new Value.Arr(List.of(
146+
new Value.Str("one"),
147+
new Value.Str("two"),
148+
Value.Int.of(3)));
149+
String out = Ktav.dumps(arr);
150+
assertNotNull(out);
151+
Value back = Ktav.loads(out);
152+
Value.Arr bArr = assertInstanceOf(Value.Arr.class, back);
153+
assertEquals(3, bArr.items().size());
154+
assertEquals(new Value.Str("one"), bArr.items().get(0));
155+
assertEquals(new Value.Str("two"), bArr.items().get(1));
156+
assertEquals(new Value.Int("3"), bArr.items().get(2));
157+
}
158+
159+
@Test
160+
void toStringForceStringsCoercesScalars() {
161+
LinkedHashMap<String, Value> entries = new LinkedHashMap<>();
162+
entries.put("count", Value.Int.of(42));
163+
entries.put("ratio", Value.Flt.of(0.5));
164+
entries.put("flag", Value.Bool.TRUE);
165+
entries.put("nothing", Value.Null.NULL);
166+
entries.put("name", new Value.Str("demo"));
167+
168+
String out = Ktav.toStringForceStrings(new Value.Obj(entries));
169+
assertNotNull(out);
170+
171+
// Round-trip — every leaf scalar must come back as Value.Str.
172+
Value back = Ktav.loads(out);
173+
Value.Obj b = assertInstanceOf(Value.Obj.class, back);
174+
assertInstanceOf(Value.Str.class, b.entries().get("count"));
175+
assertInstanceOf(Value.Str.class, b.entries().get("ratio"));
176+
assertInstanceOf(Value.Str.class, b.entries().get("flag"));
177+
assertInstanceOf(Value.Str.class, b.entries().get("nothing"));
178+
assertInstanceOf(Value.Str.class, b.entries().get("name"));
179+
180+
assertEquals(new Value.Str("42"), b.entries().get("count"));
181+
assertEquals("true", ((Value.Str) b.entries().get("flag")).value());
182+
assertEquals("null", ((Value.Str) b.entries().get("nothing")).value());
183+
assertEquals("demo", ((Value.Str) b.entries().get("name")).value());
184+
}
185+
186+
@Test
187+
void toStringForceStringsAcceptsTopLevelArray() {
188+
Value.Arr arr = new Value.Arr(List.of(
189+
Value.Int.of(1),
190+
Value.Bool.FALSE,
191+
Value.Null.NULL));
192+
String out = Ktav.toStringForceStrings(arr);
193+
assertNotNull(out);
194+
Value back = Ktav.loads(out);
195+
Value.Arr bArr = assertInstanceOf(Value.Arr.class, back);
196+
assertEquals(3, bArr.items().size());
197+
for (Value item : bArr.items()) {
198+
assertInstanceOf(Value.Str.class, item);
199+
}
200+
assertEquals("1", ((Value.Str) bArr.items().get(0)).value());
201+
assertEquals("false", ((Value.Str) bArr.items().get(1)).value());
202+
assertEquals("null", ((Value.Str) bArr.items().get(2)).value());
108203
}
109204

110205
@Test

spec

Submodule spec updated 28 files

0 commit comments

Comments
 (0)