cheatah
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.json
4// + parsers.html)
5// and the LANGUAGE features it exercises:
6// * dotted TYPE imports — `import parsers.url.Parser as Parser` aliases a class, and the
7// alias constructs (`Parser()`) and method-calls (`p.parse(...)`) like a local struct;
8// * compiler-synthesized JSON schemas — `import parsers` makes purrc emit a
9// 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 no
11// 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.
16TEST(ParsersCompileRun, UrlParserImport) {
17 e2e::expect_e2e("parsers_url_import", R"PURR(import parsers.url.Parser as Parser
18import parsers.url.Url as Url
19import io
21let p = Parser()
22let u = Url()
23if 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)
28)PURR", "example.com\n8080\n/data?x=1\n");
31// The URL parser rejects malformed input through the same .purr surface.
32TEST(ParsersCompileRun, UrlParserRejects) {
33 e2e::expect_e2e("parsers_url_rejects", R"PURR(import parsers.url.Parser as Parser
34import parsers.url.Url as Url
35import io
37let p = Parser()
38let u = Url()
39if p.parse("ftp://example.com/x", u) {
40 io.print("accepted")
41} else {
42 io.print("rejected")
44)PURR", "rejected\n");
47// The JSON DOM parser imports and runs (owning form -> self-contained Document).
48TEST(ParsersCompileRun, JsonDomParse) {
49 e2e::expect_e2e("parsers_json_dom", R"PURR(import parsers.json.Parser as JsonParser
50import io
52let jp = JsonParser()
53let d = jp.parse_owning("{\"price\": 7386.65, \"live\": true}")
54io.print("dom ok")
55)PURR", "dom ok\n");
58// THE TYPED READER: parsers.json.read parses JSON straight into a .purr struct — the schema
59// is synthesized by purrc from the struct's typed fields (str/float/bool here).
60TEST(ParsersCompileRun, TypedReader) {
61 e2e::expect_e2e("parsers_typed_reader", R"PURR(import parsers
62import io
64struct Quote {
65 symbol: str
66 price: float
67 live: bool
70let q = Quote("", 0.0, false)
71if 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)
76)PURR", "SPX\n7386.65\nTrue\n");
79// Schema synthesis composes: nested structs and list<T> fields (vector reads).
80TEST(ParsersCompileRun, TypedReaderNested) {
81 e2e::expect_e2e("parsers_typed_nested", R"PURR(import parsers
82import io
84struct Row {
85 v: float
87struct Series {
88 name: str
89 rows: list<Row>
92let rows: list<Row> = []
93let s = Series("", rows)
94if 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)
99)PURR", "av\n2\n2.5\n");
102// parsers.html escaping from .purr: escape (with and without quote), then unescape decoding
103// named + decimal + hex references back (the &copy; expectation is the two UTF-8 bytes of ©).
104TEST(ParsersCompileRun, HtmlEscapeUnescape) {
105 e2e::expect_e2e("parsers_html_escape", R"PURR(import parsers.html
106import io
108io.print(parsers.html.escape("<a href=\"x\">&'</a>"))
109io.print(parsers.html.escape("q: \"hi\"", false))
110io.print(parsers.html.unescape("&lt;p&gt; &amp; &#65;&#x42; &copy;"))
111)PURR", "&lt;a href=&quot;x&quot;&gt;&amp;&#x27;&lt;/a&gt;\nq: \"hi\"\n<p> & AB \xC2\xA9\n");
114// The tokenizing parser as DATA: a for-loop over parsers.html.parse walks every event kind
115// (decl/starttag/comment/data/endtag/startendtag) in document order — the .purr shape that
116// replaces Python's HTMLParser callback subclassing.
117TEST(ParsersCompileRun, HtmlParseWalk) {
118 e2e::expect_e2e("parsers_html_walk", R"PURR(import parsers.html
119import io
121let doc = "<!DOCTYPE html><ul id=\"m\"><!--nav--><li class=\"a\">One &amp; Two</li><br/></ul>"
122for t in parsers.html.parse(doc) {
123 io.print(t.kind + "|" + t.tag + "|" + t.data)
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");
135// The attribute helpers on a start-tag token: get_attr decodes references and matches
136// case-insensitively; has_attr sees valueless attributes and misses absent ones.
137TEST(ParsersCompileRun, HtmlAttrHelpers) {
138 e2e::expect_e2e("parsers_html_attrs", R"PURR(import parsers.html
139import io
141for t in parsers.html.parse("<a HREF=\"x&amp;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 }
148)PURR", "x&y\nTrue\nFalse\n");
151// The validating reader REJECTS malformed input (and unknown keys are skipped, not errors).
152TEST(ParsersCompileRun, TypedReaderRejectsMalformed) {
153 e2e::expect_e2e("parsers_typed_rejects", R"PURR(import parsers
154import io
156struct Quote {
157 price: float
160let q = Quote(0.0)
161if parsers.json.read("{\"price\":}", q) {
162 io.print("accepted")
163} else {
164 io.print("rejected")
166if parsers.json.read("{\"unknown\":[1,2],\"price\":3.5}", q) {
167 io.print(q.price)
169)PURR", "rejected\n3.5\n");