Source
tests/purrc/parsers_cr_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// Compile-run system tests for the `parsers` module (C++-authored: parsers.url + parsers.json4
// + parsers.html)5
// and the LANGUAGE features it exercises:6
// * dotted TYPE imports — `import parsers.url.Parser as Parser` aliases a class, and the7
// alias constructs (`Parser()`) and method-calls (`p.parse(...)`) like a local struct;8
// * compiler-synthesized JSON schemas — `import parsers` makes purrc emit a9
// cheatah::parsers::json::schema<> specialization for every struct the program defines,10
// so `parsers.json.read(text, value)` parses JSON STRAIGHT into user structs with no11
// user-written schema (the typed reader).12
// Each test writes a .purr, compiles it with purrc, runs it, and asserts exact stdout.13
#include "e2e_harness.hpp"15
// Dotted type imports: both parsers.url types, construction, method call, field access.16
TEST(ParsersCompileRun, UrlParserImport) {17
e2e::expect_e2e("parsers_url_import", R"PURR(import parsers.url.Parser as Parser18
import parsers.url.Url as Url19
import io21
let p = Parser()22
let u = Url()23
if p.parse("http://example.com:8080/data?x=1", u) {24
io.print(u.host)25
io.print(u.port)26
io.print(u.target)27
}28
)PURR", "example.com\n8080\n/data?x=1\n");29
}31
// The URL parser rejects malformed input through the same .purr surface.32
TEST(ParsersCompileRun, UrlParserRejects) {33
e2e::expect_e2e("parsers_url_rejects", R"PURR(import parsers.url.Parser as Parser34
import parsers.url.Url as Url35
import io37
let p = Parser()38
let u = Url()39
if p.parse("ftp://example.com/x", u) {40
io.print("accepted")41
} else {42
io.print("rejected")43
}44
)PURR", "rejected\n");45
}47
// The JSON DOM parser imports and runs (owning form -> self-contained Document).48
TEST(ParsersCompileRun, JsonDomParse) {49
e2e::expect_e2e("parsers_json_dom", R"PURR(import parsers.json.Parser as JsonParser50
import io52
let jp = JsonParser()53
let d = jp.parse_owning("{\"price\": 7386.65, \"live\": true}")54
io.print("dom ok")55
)PURR", "dom ok\n");56
}58
// THE TYPED READER: parsers.json.read parses JSON straight into a .purr struct — the schema59
// is synthesized by purrc from the struct's typed fields (str/float/bool here).60
TEST(ParsersCompileRun, TypedReader) {61
e2e::expect_e2e("parsers_typed_reader", R"PURR(import parsers62
import io64
struct Quote {65
symbol: str66
price: float67
live: bool68
}70
let q = Quote("", 0.0, false)71
if parsers.json.read("{\"symbol\":\"SPX\",\"price\":7386.65,\"live\":true}", q) {72
io.print(q.symbol)73
io.print(q.price)74
io.print(q.live)75
}76
)PURR", "SPX\n7386.65\nTrue\n");77
}79
// Schema synthesis composes: nested structs and list<T> fields (vector reads).80
TEST(ParsersCompileRun, TypedReaderNested) {81
e2e::expect_e2e("parsers_typed_nested", R"PURR(import parsers82
import io84
struct Row {85
v: float86
}87
struct Series {88
name: str89
rows: list<Row>90
}92
let rows: list<Row> = []93
let s = Series("", rows)94
if parsers.json.read("{\"name\":\"av\",\"rows\":[{\"v\":1.5},{\"v\":2.5}]}", s) {95
io.print(s.name)96
io.print(len(s.rows))97
io.print(s.rows[1].v)98
}99
)PURR", "av\n2\n2.5\n");100
}102
// parsers.html escaping from .purr: escape (with and without quote), then unescape decoding103
// named + decimal + hex references back (the © expectation is the two UTF-8 bytes of ©).104
TEST(ParsersCompileRun, HtmlEscapeUnescape) {105
e2e::expect_e2e("parsers_html_escape", R"PURR(import parsers.html106
import io108
io.print(parsers.html.escape("<a href=\"x\">&'</a>"))109
io.print(parsers.html.escape("q: \"hi\"", false))110
io.print(parsers.html.unescape("<p> & AB ©"))111
)PURR", "<a href="x">&'</a>\nq: \"hi\"\n<p> & AB \xC2\xA9\n");112
}114
// The tokenizing parser as DATA: a for-loop over parsers.html.parse walks every event kind115
// (decl/starttag/comment/data/endtag/startendtag) in document order — the .purr shape that116
// replaces Python's HTMLParser callback subclassing.117
TEST(ParsersCompileRun, HtmlParseWalk) {118
e2e::expect_e2e("parsers_html_walk", R"PURR(import parsers.html119
import io121
let doc = "<!DOCTYPE html><ul id=\"m\"><!--nav--><li class=\"a\">One & Two</li><br/></ul>"122
for t in parsers.html.parse(doc) {123
io.print(t.kind + "|" + t.tag + "|" + t.data)124
}125
)PURR", "decl||DOCTYPE html\n"126
"starttag|ul|\n"127
"comment||nav\n"128
"starttag|li|\n"129
"data||One & Two\n"130
"endtag|li|\n"131
"startendtag|br|\n"132
"endtag|ul|\n");133
}135
// The attribute helpers on a start-tag token: get_attr decodes references and matches136
// case-insensitively; has_attr sees valueless attributes and misses absent ones.137
TEST(ParsersCompileRun, HtmlAttrHelpers) {138
e2e::expect_e2e("parsers_html_attrs", R"PURR(import parsers.html139
import io141
for t in parsers.html.parse("<a HREF=\"x&y\" data-k>link</a>") {142
if t.kind == "starttag" {143
io.print(parsers.html.get_attr(t, "href"))144
io.print(parsers.html.has_attr(t, "data-k"))145
io.print(parsers.html.has_attr(t, "nope"))146
}147
}148
)PURR", "x&y\nTrue\nFalse\n");149
}151
// The validating reader REJECTS malformed input (and unknown keys are skipped, not errors).152
TEST(ParsersCompileRun, TypedReaderRejectsMalformed) {153
e2e::expect_e2e("parsers_typed_rejects", R"PURR(import parsers154
import io156
struct Quote {157
price: float158
}160
let q = Quote(0.0)161
if parsers.json.read("{\"price\":}", q) {162
io.print("accepted")163
} else {164
io.print("rejected")165
}166
if parsers.json.read("{\"unknown\":[1,2],\"price\":3.5}", q) {167
io.print(q.price)168
}169
)PURR", "rejected\n3.5\n");170
}