blob: bb01ef3ce5c90497e439dd548fa9f1728a984344 [file] [log] [blame] [edit]
// Copyright 2021 The Verible Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A simple code generator taking a yaml file and generating nlohmann/json
// serializable structs.
#include <cstdio>
#include <fstream>
#include <iostream>
#include <memory>
#include <regex>
#include <string>
#include <unordered_map>
#include "absl/container/flat_hash_map.h"
#include "absl/flags/flag.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "common/util/init_command_line.h"
ABSL_FLAG(std::string, output, "",
"Name of the output file. If empty, output is written to stdout");
ABSL_FLAG(std::string, class_namespace, "",
"Namespace of the generated structs");
ABSL_FLAG(std::string, json_header, "<nlohmann/json.hpp>",
"Include path to json.hpp including brackets <> or quotes \"\" "
"around.");
// Interface. Currently private, but could be moved to a header if needed.
struct Location {
const char *filename;
int line;
};
inline std::ostream &operator<<(std::ostream &out, const Location &loc) {
return out << loc.filename << ":" << loc.line << ": ";
}
struct ObjectType;
struct Property {
Property(const Location &loc, ObjectType *owner, const std::string &name,
bool is_optional, bool is_array)
: location(loc),
owner(owner),
name(name),
is_optional(is_optional),
is_array(is_array) {}
bool EqualNameValue(const Property &other) const {
return other.name == name;
}
Location location = {"<>", 0}; // Where it is defined
ObjectType *owner;
std::string name;
bool is_optional;
bool is_array;
std::string default_value;
// TODO: have alternative types
std::string type;
ObjectType *object_type = nullptr;
};
struct ObjectType {
ObjectType(const Location &loc, const std::string &name)
: location(loc), name(name) {}
Location location;
std::string name;
std::vector<std::string> extends; // name of the superclasses
std::vector<Property> properties;
std::vector<const ObjectType *> superclasses; // fixed up after all read.
};
using ObjectTypeVector = std::vector<ObjectType *>;
static bool contains(const std::string &s, char c) {
return absl::StrContains(s, c);
}
// Returns if successful.
static bool ParseObjectTypesFromFile(const std::string &filename,
ObjectTypeVector *parsed_out) {
static const std::regex emptyline_or_comment_re("^[ \t]*(#.*)?");
static const std::regex toplevel_object_re("^([a-zA-Z0-9_]+):");
// For now, let's just read up to the first type and leave out alternatives
static const std::regex property_re(
"^[ \t]+([a-zA-Z_<]+)([\\?\\+]*):[ ]*([a-zA-Z0-9_]+)[ ]*(=[ \t]*(.+))?");
Location current_location = {filename.c_str(), 0};
ObjectType *current_model = nullptr;
std::ifstream in(filename);
if (!in.good()) {
std::cerr << "Can't open " << filename << "\n";
return false;
}
std::string line;
std::smatch matches;
while (!in.eof()) {
std::getline(in, line);
current_location.line++;
if (std::regex_match(line, emptyline_or_comment_re)) continue;
if (std::regex_search(line, matches, toplevel_object_re)) {
current_model = new ObjectType(current_location, matches[1]);
parsed_out->push_back(current_model);
continue;
}
if (!current_model) {
std::cerr << current_location << "No ObjectType definition\n";
return false;
}
if (!std::regex_search(line, matches, property_re)) {
std::cerr << current_location << "This doesn't look like a property\n";
return false;
}
if (matches[1] == "<") {
current_model->extends.push_back(matches[3]);
continue;
}
Property property(current_location, current_model, matches[1],
contains(matches[2], '?'), contains(matches[2], '+'));
property.type = matches[3]; // TODO: allow multiple
property.default_value = matches[5];
current_model->properties.push_back(property);
}
return true;
}
// Validate types and return if successful.
static bool ValidateTypes(ObjectTypeVector *object_types) {
absl::flat_hash_map<std::string, ObjectType *> typeByName;
for (auto &obj : *object_types) {
// We only insert types as they come, so that we can make sure they are
// used after being defined.
auto inserted = typeByName.insert({obj->name, obj});
if (!inserted.second) {
std::cerr << obj->location << "Duplicate name; previous defined in "
<< inserted.first->second->location << "\n";
return false;
}
// Resolve superclasses
for (const auto &e : obj->extends) {
const auto &found = typeByName.find(e);
if (found == typeByName.end()) {
std::cerr << obj->location << "Unknown superclass " << e << "\n";
return false;
}
obj->superclasses.push_back(found->second);
}
for (auto &p : obj->properties) {
const std::string &t = p.type;
if (t == "object" || t == "string" || t == "integer" || t == "boolean") {
continue;
}
const auto &found = typeByName.find(t);
if (found == typeByName.end()) {
std::cerr << p.location << "Unknown object type '" << t << "'\n";
return false;
}
p.object_type = found->second;
}
// Validate that we don't have properties with the same name twice in
// one class (including superclasses)
absl::flat_hash_map<std::string, const Property *> my_property_names;
for (const auto &p : obj->properties) {
auto inserted = my_property_names.insert({p.name, &p});
if (inserted.second) continue;
std::cerr << p.location << "In class '" << obj->name
<< "' same name property '" << p.name << "' defined here\n"
<< inserted.first->second->location << " ... and here\n";
return false;
}
for (const auto &s : obj->superclasses) {
for (const auto &sp : s->properties) {
auto inserted = my_property_names.insert({sp.name, &sp});
if (inserted.second) continue;
const bool is_owner_superclass = (inserted.first->second->owner != obj);
std::cerr << obj->location << obj->name << " has duplicate property '"
<< sp.name << "'\n"
<< inserted.first->second->location << " ... found in "
<< (is_owner_superclass ? "super" : "") << "class '"
<< inserted.first->second->owner->name << "'\n"
<< sp.location << " ... and in superclass '" << s->name
<< "'\n";
return false;
}
}
}
return true;
}
std::unique_ptr<ObjectTypeVector> LoadObjectTypes(const std::string &filename) {
std::unique_ptr<ObjectTypeVector> result(new ObjectTypeVector());
if (!ParseObjectTypesFromFile(filename, result.get())) return nullptr;
if (!ValidateTypes(result.get())) return nullptr;
return result;
}
void GenerateCode(const std::string &filename,
const std::string &nlohmann_json_include,
const std::string &gen_namespace,
const ObjectTypeVector &objects, FILE *out) {
fprintf(out, "// Don't modify. Generated from %s\n", filename.c_str());
fprintf(out,
"#pragma once\n"
"#include <string>\n"
"#include <vector>\n");
fprintf(out, "#include %s\n\n", nlohmann_json_include.c_str());
if (!gen_namespace.empty()) {
fprintf(out, "namespace %s {\n", gen_namespace.c_str());
}
for (const auto &obj : objects) {
fprintf(out, "struct %s", obj->name.c_str());
bool is_first = true;
for (const auto &e : obj->extends) {
fprintf(out, "%s", is_first ? " :" : ",");
fprintf(out, " public %s", e.c_str());
is_first = false;
}
fprintf(out, " {\n");
for (const auto &p : obj->properties) {
std::string type;
if (p.object_type)
type = p.object_type->name;
else if (p.type == "string")
type = "std::string";
else if (p.type == "integer")
type = "int";
else if (p.type == "object")
type = "nlohmann::json";
else if (p.type == "boolean")
type = "bool";
// TODO: optional and array
if (type.empty()) {
std::cerr << p.location << "Not supported yet '" << p.type << "'\n";
continue;
}
if (p.is_array) {
fprintf(out, " std::vector<%s> %s", type.c_str(), p.name.c_str());
} else {
fprintf(out, " %s %s", type.c_str(), p.name.c_str());
}
if (!p.default_value.empty())
fprintf(out, " = %s", p.default_value.c_str());
fprintf(out, ";\n");
if (p.is_optional) {
fprintf(out, " bool has_%s = false; // optional property\n",
p.name.c_str());
}
}
// nlohmann::json serialization
fprintf(out, "\n");
fprintf(out, " void Deserialize(const nlohmann::json &j) {\n");
for (const auto &e : obj->extends) {
fprintf(out, " %s::Deserialize(j);\n", e.c_str());
}
for (const auto &p : obj->properties) {
int indent = 4;
std::string access_call = "j.at(\"" + p.name + "\")";
std::string access_deref = access_call + ".";
if (p.is_optional) {
fprintf(out,
"%*sif (auto found = j.find(\"%s\"); found != j.end()) {\n",
indent, "", p.name.c_str());
indent += 4;
fprintf(out, "%*shas_%s = true;\n", indent, "", p.name.c_str());
access_call = "*found";
access_deref = "found->";
}
if (p.object_type == nullptr || p.is_array) {
fprintf(out, "%*s%sget_to(%s);\n", indent, "", access_deref.c_str(),
p.name.c_str());
} else {
fprintf(out, "%*s%s.Deserialize(%s);\n", indent, "", p.name.c_str(),
access_call.c_str());
}
if (p.is_optional) {
fprintf(out, "%*s}\n", indent - 4, "");
}
}
fprintf(out, " }\n");
fprintf(out, " void Serialize(nlohmann::json *j) const {\n");
for (const auto &e : obj->extends) {
fprintf(out, " %s::Serialize(j);\n", e.c_str());
}
for (const auto &p : obj->properties) {
int indent = 4;
if (p.is_optional) {
fprintf(out, "%*sif (has_%s)", indent, "", p.name.c_str());
indent = 1;
}
if (p.object_type == nullptr || p.is_array) {
fprintf(out, "%*s(*j)[\"%s\"] = %s;\n", indent, "", p.name.c_str(),
p.name.c_str());
} else {
fprintf(out, "%*s%s.Serialize(&(*j)[\"%s\"]);\n", indent, "",
p.name.c_str(), p.name.c_str());
}
}
fprintf(out, " }\n");
fprintf(out, "};\n"); // End of struct
// functions that are picked up by the nlohmann::json serializer
// We could generate template code once for all to_json/from_json that take
// a T obj, but to limit method lookup confusion for other objects that
// might interact with the json library, let's be explicit for each struct
fprintf(out,
"inline void to_json(nlohmann::json &j, const %s &obj) "
"{ obj.Serialize(&j); }\n",
obj->name.c_str());
fprintf(out,
"inline void from_json(const nlohmann::json &j, %s &obj) "
"{ obj.Deserialize(j); }\n\n",
obj->name.c_str());
}
if (!gen_namespace.empty()) {
fprintf(out, "} // %s\n", gen_namespace.c_str());
}
}
int main(int argc, char *argv[]) {
const auto usage =
absl::StrCat("usage: ", argv[0], " [options] <protocol-spec-yaml>");
const auto file_args = verible::InitCommandLine(usage, &argc, &argv);
if (file_args.size() < 2) {
std::cerr << "Need filename";
return 1;
}
const std::string schema_filename{file_args[1].begin(), file_args[1].end()};
auto objects = LoadObjectTypes(schema_filename);
if (!objects) {
fprintf(stderr, "Couldn't parse spec\n");
return 2;
}
FILE *out = stdout;
const std::string &output = absl::GetFlag(FLAGS_output);
if (!output.empty()) {
out = fopen(output.c_str(), "w");
if (!out) {
perror("opening output file");
return 3;
}
}
GenerateCode(schema_filename, absl::GetFlag(FLAGS_json_header),
absl::GetFlag(FLAGS_class_namespace), *objects, out);
}