| // 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); |
| } |