polyplugc 0.1.1

CLI code generator for polyplug - generates type-safe bindings for multiple languages
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! TOML parsing edge-case tests for polyplugc.
//!
//! Focuses on TOML syntax anomalies that are distinct from semantic validation:
//! missing closing brackets, invalid escape sequences, mixed table/array syntax,
//! empty input, comments-only input, invalid Unicode escapes, very long lines,
//! and deeply nested tables.
//!
//! Run with:
//!   cargo test --test toml_malformed --package polyplugc

#![allow(clippy::expect_used)]

use polyplug_codegen::error::PolyplugcError;
use polyplugc::parser::{parse_api_str, parse_bundle_str};

// ─── Helpers ─────────────────────────────────────────────────────────────────

/// Assert that `parse_api_str` fails and return the error.
fn api_err(toml: &str) -> PolyplugcError {
    parse_api_str(toml).expect_err("expected parse error but got Ok")
}

/// Assert that `parse_bundle_str` fails and return the error.
fn bundle_err(toml: &str) -> PolyplugcError {
    parse_bundle_str(toml).expect_err("expected parse error but got Ok")
}

// ═══════════════════════════════════════════════════════════════════════════
// 1. Missing closing bracket variants
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn missing_closing_bracket_single_table() {
    // `[bundle` missing the closing `]` — TOML parse error.
    let err: PolyplugcError = bundle_err("[bundle\nname = \"x\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for missing `]` in table header, got {err:?}",
    );
}

#[test]
fn missing_closing_bracket_array_of_tables() {
    // `[[contract` missing the closing `]]` — TOML parse error.
    let err: PolyplugcError = api_err("[[contract\nname = \"x\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for missing `]]` in array-of-tables header, got {err:?}",
    );
}

#[test]
fn missing_closing_bracket_nested_subtable() {
    // `[[contract.functions` missing the closing `]]`.
    let err: PolyplugcError = api_err(concat!(
        "[[contract]]\nname = \"x\"\nversion = \"1.0\"\n\n",
        "[[contract.functions\nname = \"f\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for missing `]]` in sub-array header, got {err:?}",
    );
}

#[test]
fn unclosed_inline_array_value() {
    // Value is an unclosed inline array: `implements = ["x"`.
    let err: PolyplugcError = bundle_err(concat!(
        "[bundle]\nname = \"b\"\nversion = \"1.0\"\n\n",
        "[[plugin]]\nname = \"p\"\nimplements = [\"x\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for unclosed inline array, got {err:?}",
    );
}

#[test]
fn unclosed_inline_table_value() {
    // Inline table `{name = "x"` missing the closing `}`.
    let err: PolyplugcError = api_err("[[contract]]\nname = {value = \"x\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for unclosed inline table, got {err:?}",
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 2. Invalid escape sequences in string values
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn invalid_escape_sequence_backslash_q() {
    // `\q` is not a valid TOML escape sequence.
    let err: PolyplugcError = api_err("[[contract]]\nname = \"bad\\qescape\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for `\\q` escape, got {err:?}",
    );
}

#[test]
fn invalid_escape_sequence_backslash_a() {
    // `\a` is not a valid TOML escape sequence (unlike C).
    let err: PolyplugcError = bundle_err("[bundle]\nname = \"bad\\aescape\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for `\\a` escape, got {err:?}",
    );
}

#[test]
fn invalid_escape_sequence_lone_backslash() {
    // A lone trailing backslash before end-of-string is invalid.
    let err: PolyplugcError = api_err("[[contract]]\nname = \"trailing\\\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for lone trailing backslash, got {err:?}",
    );
}

#[test]
fn valid_escape_sequences_accepted() {
    // Standard TOML escapes: `\\`, `\"`, `\n`, `\t`, `\r` must all be accepted by
    // the TOML lexer. The resulting contract name is not a valid identifier, so
    // name validation (not the TOML parser) rejects it — which proves the escape
    // was lexed successfully rather than producing a TOML parse error.
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str(
        "[[contract]]\nname = \"esc\\\\slash\\\"quote\\nnewline\\ttab\"\nversion = \"1.0\"",
    );
    assert!(
        matches!(result, Err(PolyplugcError::InvalidIdentifier { .. })),
        "expected TOML escapes to lex (then fail identifier validation), got {result:?}"
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 3. Mixed table / array-of-tables syntax collisions
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn mixed_table_and_array_of_tables_same_key() {
    // Defining `[contract]` and then `[[contract]]` for the same key violates the TOML spec.
    let err: PolyplugcError = api_err(concat!(
        "[contract]\nname = \"single\"\nversion = \"1.0\"\n\n",
        "[[contract]]\nname = \"array\"\nversion = \"1.0\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for mixed `[contract]` + `[[contract]]`, got {err:?}",
    );
}

#[test]
fn redefining_existing_table_header() {
    // Duplicate `[bundle]` header is illegal in TOML.
    let err: PolyplugcError = bundle_err(concat!(
        "[bundle]\nname = \"first\"\nversion = \"1.0\"\n\n",
        "[bundle]\nname = \"second\"\nversion = \"2.0\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for duplicate `[bundle]` header, got {err:?}",
    );
}

#[test]
fn array_element_defined_before_array_header() {
    // Assigning `contract.name` as a dotted key then using `[[contract]]`
    // is a TOML conflict (implicitly created vs. explicitly created).
    let err: PolyplugcError = api_err(concat!(
        "contract.name = \"dotted\"\n\n",
        "[[contract]]\nname = \"array\"\nversion = \"1.0\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for dotted-key vs array-of-tables conflict, got {err:?}",
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 4. Empty TOML
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn empty_string_is_valid_api_toml() {
    // An empty API schema is semantically valid — no contracts, no types.
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str("");
    assert!(
        result.is_ok(),
        "expected empty string to parse as empty API, got {result:?}"
    );
    let ir: polyplugc::ir::ValidatedIr = result.expect("parse");
    assert_eq!(ir.contracts.len(), 0, "expected zero contracts");
    assert_eq!(ir.types.len(), 0, "expected zero types");
    assert_eq!(ir.enums.len(), 0, "expected zero enums");
}

#[test]
fn empty_string_is_invalid_bundle_toml() {
    // A bundle.toml MUST have a `[bundle]` section — empty string should fail.
    let err: PolyplugcError = bundle_err("");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for empty bundle TOML, got {err:?}",
    );
}

#[test]
fn newlines_only_is_valid_api_toml() {
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str("\n\n\n");
    assert!(
        result.is_ok(),
        "expected newlines-only to parse as empty API, got {result:?}"
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 5. Comments-only TOML
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn comments_only_is_valid_api_toml() {
    let toml: &str = "# polyplug api schema\n# no contracts defined yet\n";
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str(toml);
    assert!(
        result.is_ok(),
        "expected comments-only API TOML to succeed, got {result:?}"
    );
}

#[test]
fn comments_only_is_invalid_bundle_toml() {
    // Comments but no `[bundle]` section — must fail.
    let err: PolyplugcError = bundle_err("# just a comment\n# another comment\n");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for comments-only bundle TOML, got {err:?}",
    );
}

#[test]
fn comment_after_value_is_valid() {
    // Inline comments after values are valid TOML.
    let toml: &str = concat!(
        "[[contract]] # define a contract\n",
        "name = \"svc.foo\" # the name\n",
        "version = \"1.0\" # the version\n",
    );
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str(toml);
    assert!(
        result.is_ok(),
        "expected inline comments after values to be accepted, got {result:?}"
    );
}

#[test]
fn comment_mid_key_value_is_invalid() {
    // A `#` inside a quoted key name is still a valid character inside a string,
    // but a `#` between the key and `=` terminates the line early — TOML error.
    let err: PolyplugcError = api_err("[[contract]]\nname # comment = \"x\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for `#` between key and `=`, got {err:?}",
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 6. Invalid Unicode escape sequences
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn unicode_escape_with_too_few_hex_digits() {
    // `\uXXX` requires exactly 4 hex digits; 3 is invalid.
    let err: PolyplugcError = api_err("[[contract]]\nname = \"bad\\u004\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for `\\u` with 3 hex digits, got {err:?}",
    );
}

#[test]
fn unicode_escape_with_non_hex_digit() {
    // `\uXXXG` — `G` is not a valid hex digit.
    let err: PolyplugcError = api_err("[[contract]]\nname = \"bad\\u000G\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for `\\u000G` non-hex digit, got {err:?}",
    );
}

#[test]
fn unicode_escape_surrogate_pair_rejected() {
    // `\uD800` is a lone surrogate — invalid in TOML (must be valid Unicode scalar).
    let err: PolyplugcError = api_err("[[contract]]\nname = \"\\uD800\"\nversion = \"1.0\"");
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for lone surrogate `\\uD800`, got {err:?}",
    );
}

#[test]
fn unicode_escape_valid_codepoint_accepted() {
    // `\u0041` is 'A' — a perfectly valid Unicode scalar value.
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> =
        parse_api_str("[[contract]]\nname = \"\\u0041BC\"\nversion = \"1.0\"");
    assert!(
        result.is_ok(),
        "expected valid \\u0041 Unicode escape to be accepted, got {result:?}"
    );
}

#[test]
fn long_unicode_escape_u_uppercase_valid() {
    // `\U0001F600` (8 hex digits) — valid TOML long Unicode escape. The TOML
    // lexer must accept it; the resulting name contains a non-identifier
    // codepoint (emoji), so name validation rejects it afterwards. An
    // InvalidIdentifier (not a TomlParseError) proves the escape lexed.
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> =
        parse_api_str("[[contract]]\nname = \"emoji\\U0001F600end\"\nversion = \"1.0\"");
    assert!(
        matches!(result, Err(PolyplugcError::InvalidIdentifier { .. })),
        "expected \\U0001F600 long Unicode escape to lex (then fail identifier validation), got {result:?}"
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 7. Very long lines
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn very_long_name_value_accepted() {
    // A 10 000-character name string — TOML has no line-length limit.
    let long_name: String = "a".repeat(10_000);
    let toml: String = format!("[[contract]]\nname = \"{long_name}\"\nversion = \"1.0\"\n");
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str(&toml);
    assert!(
        result.is_ok(),
        "expected very long name string to be accepted, got {result:?}"
    );
    let ir: polyplugc::ir::ValidatedIr = result.expect("parse");
    assert_eq!(ir.contracts[0].name.len(), 10_000);
}

#[test]
fn very_long_comment_line_accepted() {
    // A comment that is 50 000 characters long should not cause an error.
    let long_comment: String = format!("# {}", "x".repeat(50_000));
    let toml: String = format!("{long_comment}\n[[contract]]\nname = \"svc\"\nversion = \"1.0\"\n");
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str(&toml);
    assert!(
        result.is_ok(),
        "expected very long comment line to be accepted, got {result:?}"
    );
}

#[test]
fn very_long_key_name_invalid() {
    // A key that is not a valid TOML bare key (contains spaces) must fail.
    // We rely on the space to make the key malformed rather than just "long".
    let long_key: String = format!("{} extra", "a".repeat(1_000));
    let toml: String = format!("[[contract]]\n{long_key} = \"v\"\nversion = \"1.0\"\n");
    let err: PolyplugcError = api_err(&toml);
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for key with embedded space, got {err:?}",
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// 8. Nested tables
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn deeply_nested_dotted_keys_accepted() {
    // TOML supports dotted keys; here we use them on a non-ABI field.
    // The `[bundle]` section can have dotted-path keys.
    // We verify the parser doesn't crash on deep dotting even if unused.
    let toml: &str = concat!(
        "[bundle]\n",
        "name = \"nested-bundle\"\n",
        "version = \"1.0\"\n",
        "extra.deep.key = \"ignored\"\n",
    );
    // The `RawBundleMeta` struct has `#[serde(default)]`-annotated optional fields;
    // depending on the schema this may succeed or surface a `TomlParseError`.
    // We only assert it doesn't panic.
    let _result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_bundle_str(toml);
    // No assertion on Ok/Err — just confirm no panic / no crash.
}

#[test]
fn nested_inline_table_in_array_is_malformed() {
    // An inline table that is itself unclosed inside an array is invalid.
    let err: PolyplugcError = bundle_err(concat!(
        "[bundle]\nname = \"b\"\nversion = \"1.0\"\n\n",
        "[[plugin]]\nname = \"p\"\n",
        "implements = [{contract = \"svc\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for unclosed inline table in array, got {err:?}",
    );
}

#[test]
fn super_table_key_conflict_after_dotted_key() {
    // Assigning `bundle.name` via a dotted key first, then defining `[bundle]`
    // and re-assigning `name` is a TOML duplicate-key violation.
    let err: PolyplugcError = bundle_err(concat!(
        "bundle.name = \"first\"\n\n",
        "[bundle]\nname = \"second\"\nversion = \"1.0\"\n",
    ));
    assert!(
        matches!(err, PolyplugcError::TomlParseError { .. }),
        "expected TomlParseError for dotted-key then [bundle] name conflict, got {err:?}",
    );
}

#[test]
fn contract_functions_params_nested_array_round_trip() {
    // A fully-specified nested structure [[contract]] → [[contract.functions]] →
    // [[contract.functions.params]] must parse cleanly.
    let toml: &str = concat!(
        "[[contract]]\n",
        "name = \"math.ops\"\n",
        "version = \"1.0\"\n\n",
        "[[contract.functions]]\n",
        "name = \"add\"\n\n",
        "[[contract.functions.params]]\n",
        "name = \"a\"\n",
        "type = \"u32\"\n\n",
        "[[contract.functions.params]]\n",
        "name = \"b\"\n",
        "type = \"u32\"\n",
    );
    let result: Result<polyplugc::ir::ValidatedIr, PolyplugcError> = parse_api_str(toml);
    assert!(
        result.is_ok(),
        "expected deeply-nested array-of-tables round-trip to succeed, got {result:?}"
    );
    let ir: polyplugc::ir::ValidatedIr = result.expect("parse");
    assert_eq!(ir.contracts[0].functions[0].params.len(), 2);
}