sots-engine/tests/mars_text/dump_json.cpp

200 lines
6.5 KiB
C++

// dump_json <kv|rows|manifest|csv> <file>
// Emits the parse result as JSON in the same shape as tests/mars_text/oracle/dump.py
// so the two can be compared structurally. Bytes >= 0x80 are written as
// \u00XX (one escape per byte) on both sides.
#include <cstdio>
#include <fstream>
#include <iterator>
#include <map>
#include <string>
#include <vector>
#include "mars/text/csv.h"
#include "mars/text/flat_kv.h"
#include "mars/text/manifest.h"
using namespace mars::text;
namespace {
void json_string(std::string_view s, std::string& out) {
out.push_back('"');
for (unsigned char c : s) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20 || c >= 0x7f) {
char buf[8];
std::snprintf(buf, sizeof buf, "\\u%04x", c);
out += buf;
} else {
out.push_back(static_cast<char>(c));
}
}
}
out.push_back('"');
}
void json_token(const Token& t, std::string& out) {
switch (t.kind()) {
case ValueKind::Int: out += std::to_string(*t.as_int()); break;
case ValueKind::Float: {
char buf[64];
std::snprintf(buf, sizeof buf, "%.17g", *t.as_float());
std::string s = buf;
if (s.find_first_of(".eEn") == std::string::npos) s += ".0"; // keep it a JSON float
out += s;
break;
}
case ValueKind::Bool: out += *t.as_bool() ? "true" : "false"; break;
case ValueKind::Bareword:
case ValueKind::String: json_string(t.text, out); break;
}
}
void json_tokens(const std::vector<Token>& toks, std::string& out) {
out.push_back('[');
for (std::size_t i = 0; i < toks.size(); ++i) {
if (i) out.push_back(',');
json_token(toks[i], out);
}
out.push_back(']');
}
std::string dump_kv(const FlatKV& kv) {
// engine rule: keys compared case-insensitively, first occurrence wins,
// reported under its first spelling (the reference reader's dict shape)
std::map<std::string, const KvEntry*> first_seen;
std::vector<const KvEntry*> order;
for (const KvEntry& e : kv.entries())
if (first_seen.emplace(fold_case(e.key), &e).second) order.push_back(&e);
std::string out = "{\"values\":{";
bool first = true;
for (const KvEntry* e : order) {
if (!first) out.push_back(',');
first = false;
json_string(e->key, out);
out.push_back(':');
json_token(e->value, out);
}
out += "},\"duplicates\":{";
first = true;
for (const auto& [key, lines] : kv.duplicates()) {
if (!first) out.push_back(',');
first = false;
json_string(key, out);
out += ":[";
for (std::size_t i = 0; i < lines.size(); ++i) out += (i ? "," : "") + std::to_string(lines[i]);
out.push_back(']');
}
out += "}}";
return out;
}
std::string dump_rows(const Rows& rows) {
std::string out = "{\"rows\":[";
for (std::size_t i = 0; i < rows.size(); ++i) {
if (i) out.push_back(',');
json_tokens(rows[i].tokens, out);
}
out += "]}";
return out;
}
const char* problem_kind(Problem::Kind k) {
switch (k) {
case Problem::Kind::Unrecognised: return "unrecognised";
case Problem::Kind::DuplicateId: return "duplicate_id";
case Problem::Kind::DeletedAndAssigned: return "deleted_and_assigned";
case Problem::Kind::UnbalancedQuote: return "unbalanced_quote";
case Problem::Kind::UnterminatedQuotedCell: return "unterminated_quoted_cell";
case Problem::Kind::DuplicateKey: return "duplicate_key";
case Problem::Kind::DroppedTrailingPair: return "dropped_trailing_pair";
case Problem::Kind::UnterminatedQuote: return "unterminated_quote";
case Problem::Kind::SkippedBlock: return "skipped_block";
}
return "?";
}
std::string dump_manifest(const Result<Manifest>& r) {
std::string out = "{\"entries\":[";
bool first = true;
for (const ManifestEntry& e : r.value.entries()) {
if (!first) out.push_back(',');
first = false;
out += "[" + std::to_string(e.id) + ",";
json_string(e.name, out);
out.push_back(']');
}
out += "],\"deleted\":[";
const auto& del = r.value.deleted();
for (std::size_t i = 0; i < del.size(); ++i) out += (i ? "," : "") + std::to_string(del[i]);
out += "],\"problems\":[";
first = true;
for (const Problem& p : r.problems) {
if (!first) out.push_back(',');
first = false;
out += "{\"line\":" + (p.line ? std::to_string(p.line) : std::string("null")) + ",\"kind\":\"" + problem_kind(p.kind) +
"\",\"id\":" + (p.id >= 0 ? std::to_string(p.id) : std::string("null")) + "}";
}
out += "]}";
return out;
}
std::string dump_csv(const Csv& csv) {
std::string out = "{\"header\":";
if (csv.header) {
out.push_back('[');
for (std::size_t i = 0; i < csv.header->size(); ++i) {
if (i) out.push_back(',');
json_string((*csv.header)[i], out);
}
out.push_back(']');
} else {
out += "null";
}
out += ",\"rows\":[";
for (std::size_t i = 0; i < csv.rows.size(); ++i) {
if (i) out.push_back(',');
out.push_back('[');
for (std::size_t j = 0; j < csv.rows[i].cells.size(); ++j) {
if (j) out.push_back(',');
json_string(csv.rows[i].cells[j], out);
}
out.push_back(']');
}
out += "]}";
return out;
}
} // namespace
int main(int argc, char** argv) {
if (argc != 3) {
std::fprintf(stderr, "usage: dump_json <kv|rows|manifest|csv> <file>\n");
return 2;
}
std::ifstream in(argv[2], std::ios::binary);
if (!in) {
std::fprintf(stderr, "cannot read %s\n", argv[2]);
return 2;
}
std::string text((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
std::string kind = argv[1];
std::string out;
if (kind == "kv") out = dump_kv(parse_flat_kv(text).value);
else if (kind == "rows") out = dump_rows(parse_rows(text).value);
else if (kind == "manifest") out = dump_manifest(parse_manifest(text));
else if (kind == "csv") out = dump_csv(parse_csv(text).value);
else {
std::fprintf(stderr, "unknown kind %s\n", argv[1]);
return 2;
}
std::fwrite(out.data(), 1, out.size(), stdout);
std::fputc('\n', stdout);
return 0;
}