Add a validator subclass specialized for validating WML schemas against the WML schema schema

This commit is contained in:
Celtic Minstrel 2018-11-03 15:26:22 -04:00
parent 4674e97eb6
commit 52c4dc0e3d
12 changed files with 473 additions and 24 deletions

View file

@ -39,7 +39,6 @@
[/tag]
[tag]
name="$generic_lua_component"
engine=lua
{SIMPLE_KEY code string}
{DATA_TAG args 0 1}
[/tag]

View file

@ -238,7 +238,7 @@
{SIMPLE_KEY begin s_real}
{SIMPLE_KEY end s_real}
{SIMPLE_KEY spend_all_gold s_int}
{SIMPLE_KEY save_on_negative_income sbool}
{SIMPLE_KEY save_on_negative_income s_bool}
)}
{AI_FACET_TAG recruitment_instructions (

View file

@ -164,10 +164,10 @@
{AI_MODIFY_MATCH_ASPECT villages_per_scout int}
{AI_MODIFY_MATCH_ASPECT attack_depth int}
{AI_MODIFY_MATCH_ASPECT recruitment_randomness int}
{AI_MODIFY_MATCH_ASPECT grouping ai_grouping}
{AI_MODIFY_MATCH_ASPECT advancements string_list}
{AI_MODIFY_MATCH_ASPECT recruitment_more string_list}
{AI_MODIFY_MATCH_ASPECT recruitment_pattern string_list}
{AI_MODIFY_MATCH_ASPECT grouping grouping}
{AI_MODIFY_MATCH_ASPECT advancements string}
{AI_MODIFY_MATCH_ASPECT recruitment_more string}
{AI_MODIFY_MATCH_ASPECT recruitment_pattern string}
{AI_MODIFY_MATCH_ASPECT avoid avoid}
{AI_MODIFY_MATCH_ASPECT leader_goal leader_goal}
{AI_MODIFY_MATCH_ASPECT recruitment_instructions recruitment_instructions}

View file

@ -72,8 +72,8 @@
[tag]
name="fog_override"
max=infinite
{SIMPLE_KEY x int_list}
{SIMPLE_KEY y int_list}
{SIMPLE_KEY x range_list}
{SIMPLE_KEY y range_list}
[/tag]
[tag]
name="ai"
@ -289,7 +289,7 @@
{SIMPLE_KEY map_data map_data}
{DEFAULT_KEY turns turns unlimited}
{DEFAULT_KEY turn_at int 1}
{DEFAULT_KEY random_start_time bool,int_list no} # Note: Is it random_start_time or random_starting_time? (There's some uses of the latter)
{DEFAULT_KEY random_start_time bool,int_list no}
{DEPRECATED_KEY music string}
{SIMPLE_KEY defeat_music string}
{SIMPLE_KEY victory_music string}

View file

@ -47,7 +47,7 @@
{LINK_TAG "lua"}
[tag]
name="and,or,not"
super="$condition_wml"
super="$conditional_wml"
[/tag]
any_tag=yes
[/tag]

View file

@ -1,7 +1,7 @@
[tag]
name="editor_group"
max=unlimited
max=infinite
{REQUIRED_KEY id string}
{SIMPLE_KEY name t_string}
{SIMPLE_KEY icon t_string}
@ -10,11 +10,11 @@
[tag]
name="item_group"
max=unlimited
max=infinite
super="editor_group"
[tag]
name="item"
max=unlimited
max=infinite
super="scenario/item"
{REQUIRED_KEY id string}
{SIMPLE_KEY name t_string}

View file

@ -90,6 +90,14 @@
[/element]
[/list]
[/type]
[type]
name=int_list
[list]
[element]
link=unsigned
[/element]
[/list]
[/type]
[type]
name=prog_string
value=".*"

210
data/schema/schema.cfg Normal file
View file

@ -0,0 +1,210 @@
{./macros.cfg}
[wml_schema]
[type]
name="regex"
value=".*"
[/type]
[type]
name="glob"
value=".*"
[/type]
[type]
name="string"
value=".*"
[/type]
[type]
name="id"
value="[a-zA-Z0-9_~$]+"
[/type]
[type]
name="path"
[list]
min=1
split="/"
[element]
link="id"
[/element]
[/list]
[/type]
[type]
name="path_list"
[list]
min=1
[element]
link="path"
[/element]
[/list]
[/type]
[type]
name="id_list"
[list]
min=1
[element]
link="id"
[/element]
[/list]
[/type]
[type]
name="int"
value="\d+"
[/type]
[type]
name="inf"
value="infinite"
[/type]
[type]
name="bool"
value="yes|no|true|false"
[/type]
[tag]
name="root"
min=1
[tag]
name="wml_schema"
min=1
[tag]
name="type"
max=infinite
super="type"
{REQUIRED_KEY name id}
[if]
[union]
[/union]
[then]
[tag]
name="union"
min=1
[tag]
name="type"
max=infinite
super="wml_schema/type"
# Override the required name with an optional name
# Technically name is not really allowed here at all,
# but the schema can't override a supertag key with its absence.
{SIMPLE_KEY name string}
[/tag]
[/tag]
[/then]
[elseif]
[intersection]
[/intersection]
[then]
[tag]
name="intersection"
min=1
{LINK_TAG "wml_schema/type"}
[tag]
name="type"
max=infinite
super="wml_schema/type"
# Override the required name with an optional name
# Technically name is not really allowed here at all,
# but the schema can't override a supertag key with its absence.
{SIMPLE_KEY name string}
[/tag]
[/tag]
[/then]
[/elseif]
[elseif]
[list]
[/list]
[then]
[tag]
name="list"
min=1
{DEFAULT_KEY min int 0}
{DEFAULT_KEY max int,inf infinite}
{DEFAULT_KEY split regex "\s*,\s*"}
[tag]
name="element"
max=infinite
super="wml_schema/type"
# Override the required name with an optional name
# Technically name is not really allowed here at all,
# but the schema can't override a supertag key with its absence.
{SIMPLE_KEY name string}
[/tag]
[/tag]
[/then]
[/elseif]
[elseif]
glob_on_value=*
[then]
{SIMPLE_KEY value regex}
[/then]
[/elseif]
[else]
{SIMPLE_KEY link id}
[/else]
[/if]
[/tag]
[tag]
name="tag"
min=1
{REQUIRED_KEY name glob}
{DEFAULT_KEY min int 0}
{DEFAULT_KEY max int,inf 1}
{SIMPLE_KEY super path_list}
{DEFAULT_KEY any_tag bool no}
{DEFAULT_KEY deprecated bool no}
[tag]
name="key"
max=infinite
{REQUIRED_KEY name glob}
{REQUIRED_KEY type id_list}
{DEFAULT_KEY mandatory bool no}
{SIMPLE_KEY default string}
{DEFAULT_KEY deprecated bool no}
[/tag]
{LINK_TAG "wml_schema/tag"}
[tag]
name="link"
max=infinite
{REQUIRED_KEY name path}
[/tag]
[tag]
name="switch"
max=infinite
{REQUIRED_KEY key id}
[tag]
name="case"
max=infinite
super="wml_schema/tag"
{REQUIRED_KEY value string}
{DEFAULT_KEY trigger_if_missing bool no}
[/tag]
[tag]
name="else"
super="wml_schema/tag"
[/tag]
[/tag]
[tag]
name="if"
max="infinite"
any_tag=yes
{ANY_KEY string}
[tag]
name="then"
super="wml_schema/tag"
[/tag]
[tag]
name="elseif"
max=infinite
super="wml_schema/tag"
any_tag=yes
{ANY_KEY string}
[tag]
name="then"
min=1
super="wml_schema/tag"
[/tag]
[/tag]
[tag]
name="else"
super="wml_schema/tag"
[/tag]
[/tag]
[/tag]
[/tag]
[/tag]
[/wml_schema]

View file

@ -40,7 +40,13 @@
[type]
name=color
value="(?:2[0-5][0-5]|[01]?\d?\d)[.,]\s*(?:2[0-5][0-5]|[01]?\d?\d)[.,]\s*(?:2[0-5][0-5]|[01]?\d?\d)[.,]\s*(?:2[0-5][0-5]|[01]?\d?\d)"
[list]
min=3
max=4
[element]
value="(?:2[0-5][0-5]|[01]?\d?\d)\s*"
[/element]
[/list]
[/type]
[type]

View file

@ -31,7 +31,7 @@
{SIMPLE_KEY advances_to string_list}
{SIMPLE_KEY race string}
{SIMPLE_KEY undead_variation string}
{SIMPLE_KEY usage usage}
{SIMPLE_KEY usage ai_usage}
{SIMPLE_KEY zoc s_bool}
{SIMPLE_KEY ellipse string}
{DEPRECATED_KEY ai_special string} # Not documented

View file

@ -20,6 +20,7 @@
#include "serialization/preprocessor.hpp"
#include "serialization/string_utils.hpp"
#include "wml_exception.hpp"
#include <tuple>
namespace schema_validation
{
@ -115,6 +116,18 @@ static void wrong_value_error(const std::string& file,
print_output(ss.str(), flag_exception);
}
static void wrong_path_error(const std::string& file,
int line,
const std::string& tag,
const std::string& key,
const std::string& value,
bool flag_exception)
{
std::ostringstream ss;
ss << "Unknown path reference '" << value << "' in key '" << key << "=' in tag [" << tag << "]\n" << at(file, line) << "\n";
print_output(ss.str(), flag_exception);
}
static void wrong_type_error(const std::string & file, int line,
const std::string & tag,
const std::string & key,
@ -130,14 +143,10 @@ schema_validator::~schema_validator()
{
}
schema_validator::schema_validator(const std::string& config_file_name)
schema_validator::schema_validator(const std::string& config_file_name, bool validate_schema)
: config_read_(false)
, create_exceptions_(strict_validation_enabled)
, root_()
, stack_()
, counter_()
, cache_()
, types_()
, validate_schema_(validate_schema)
{
if(!read_config_file(config_file_name)) {
ERR_VL << "Schema file " << config_file_name << " was not read." << std::endl;
@ -156,9 +165,13 @@ bool schema_validator::read_config_file(const std::string& filename)
{
config cfg;
try {
std::unique_ptr<abstract_validator> validator;
if(validate_schema_) {
validator.reset(new schema_self_validator());
}
preproc_map preproc(game_config::config_cache::instance().get_preproc_map());
filesystem::scoped_istream stream = preprocess_file(filename, &preproc);
read(cfg, *stream);
read(cfg, *stream, validator.get());
} catch(const config::error& e) {
ERR_VL << "Failed to read file " << filename << ":\n" << e.what() << "\n";
return false;
@ -353,6 +366,182 @@ void schema_validator::print(message_info& el)
case WRONG_TYPE:
wrong_type_error(el.file, el.line, el.tag, el.key, el.value, create_exceptions_);
break;
case WRONG_PATH:
wrong_path_error(el.file, el.line, el.tag, el.key, el.value, create_exceptions_);
break;
}
}
schema_self_validator::schema_self_validator()
: schema_validator(filesystem::get_wml_location("schema/schema.cfg"), false)
, type_nesting_()
, condition_nesting_()
{}
void schema_self_validator::open_tag(const std::string& name, const config& parent, int start_line, const std::string& file, bool addition)
{
schema_validator::open_tag(name, parent, start_line, file, addition);
if(name == "type") {
type_nesting_++;
}
if(condition_nesting_ == 0) {
if(name == "if" || name == "switch") {
condition_nesting_ = 1;
} else if(name == "tag") {
tag_stack_.emplace();
}
} else {
condition_nesting_++;
}
}
void schema_self_validator::close_tag()
{
if(have_active_tag()) {
auto tag_name = active_tag().get_name();
if(tag_name == "type") {
type_nesting_--;
} else if(condition_nesting_ == 0 && tag_name == "tag") {
tag_stack_.pop();
}
}
if(condition_nesting_ > 0) {
condition_nesting_--;
}
schema_validator::close_tag();
}
bool schema_self_validator::tag_path_exists(const config& cfg, const reference& ref) {
std::vector<std::string> path = utils::split(ref.value_, '/');
std::string suffix = path.back();
path.pop_back();
while(!path.empty()) {
std::string prefix = utils::join(path, "/");
auto link = links_.find(prefix);
if(link != links_.end()) {
std::string new_path = link->second + "/" + suffix;
if(defined_tag_paths_.count(new_path) > 0) {
return true;
}
path = utils::split(new_path, '/');
suffix = path.back();
//suffix = link->second + "/" + suffix;
} else {
auto supers = derivations_.equal_range(prefix);
if(supers.first != supers.second) {
reference super_ref = ref;
for( ; supers.first != supers.second; ++supers.first) {
super_ref.value_ = supers.first->second + "/" + suffix;
if(tag_path_exists(cfg, super_ref)) {
return true;
}
}
}
std::string new_path = prefix + "/" + suffix;
if(defined_tag_paths_.count(new_path) > 0) {
return true;
}
suffix = path.back() + "/" + suffix;
}
path.pop_back();
}
return false;
}
void schema_self_validator::validate(const config& cfg, const std::string& name, int start_line, const std::string& file)
{
if(type_nesting_ == 1 && name == "type") {
defined_types_.insert(cfg["name"]);
} else if(name == "wml_schema") {
using namespace std::placeholders;
std::vector<reference> missing_types = referenced_types_, missing_tags = referenced_tag_paths_;
// Remove all the known types
missing_types.erase(std::remove_if(missing_types.begin(), missing_types.end(), std::bind(&reference::match, _1, std::cref(defined_types_))), missing_types.end());
// Remove all the known tags. This is more complicated since links behave similar to a symbolic link.
// In other words, the presence of links means there may be more than one way to refer to a given tag.
// But that's not all! It's possible to refer to a tag through a derived tag even if it's actually defined in the base tag.
auto end = std::remove_if(missing_tags.begin(), missing_tags.end(), std::bind(&reference::match, _1, std::cref(defined_tag_paths_)));
missing_tags.erase(std::remove_if(missing_tags.begin(), end, std::bind(&schema_self_validator::tag_path_exists, this, std::ref(cfg), _1)), missing_tags.end());
std::sort(missing_types.begin(), missing_types.end());
std::sort(missing_tags.begin(), missing_tags.end());
static const config dummy;
for(auto& ref : missing_types) {
std::string name;
if(ref.tag_ == "key") {
name = "type";
} else {
name = "link";
}
queue_message(dummy, WRONG_TYPE, ref.file_, ref.line_, 0, ref.tag_, name, ref.value_);
}
for(auto& ref : missing_tags) {
std::string name;
if(ref.tag_ == "tag") {
name = "super";
} else if(ref.tag_ == "link") {
name = "name";
}
queue_message(dummy, WRONG_PATH, ref.file_, ref.line_, 0, ref.tag_, name, ref.value_);
}
}
schema_validator::validate(cfg, name, start_line, file);
}
void schema_self_validator::validate_key(const config& cfg, const std::string& name, const std::string& value, int start_line, const std::string& file)
{
schema_validator::validate_key(cfg, name, value, start_line, file);
if(have_active_tag() && !active_tag().get_name().empty() && is_valid()) {
const std::string& tag_name = active_tag().get_name();
if(tag_name == "key" && name == "type" ) {
for(auto& possible_type : utils::split(cfg["type"])) {
referenced_types_.emplace_back(possible_type, file, start_line, tag_name);
}
} else if((tag_name == "type" || tag_name == "element") && name == "link") {
referenced_types_.emplace_back(cfg["link"], file, start_line, tag_name);
} else if(tag_name == "link" && name == "name") {
referenced_tag_paths_.emplace_back(cfg["name"], file, start_line, tag_name);
std::string link_name = utils::split(cfg["name"], '/').back();
links_.emplace(current_path() + "/" + link_name, cfg["name"]);
} else if(tag_name == "tag" && name == "super") {
for(auto super : utils::split(cfg["super"])) {
referenced_tag_paths_.emplace_back(super, file, start_line, tag_name);
derivations_.emplace(std::make_pair(current_path(), super));
}
} else if(condition_nesting_ == 0 && tag_name == "tag" && name == "name") {
tag_stack_.top() = value;
defined_tag_paths_.insert(current_path());
}
}
}
std::string schema_self_validator::current_path() const
{
std::stack<std::string> temp = tag_stack_;
std::deque<std::string> path;
while(!temp.empty()) {
path.push_front(temp.top());
temp.pop();
}
if(path.front() == "root") {
path.pop_front();
}
return utils::join(path, "/");
}
bool schema_self_validator::reference::operator<(const reference& other)
{
return std::make_tuple(file_, line_) < std::make_tuple(other.file_, other.line_);
}
bool schema_self_validator::reference::match(const std::set<std::string>& with)
{
return with.count(value_) > 0;
}
bool schema_self_validator::reference::can_find(const class_tag& root, const config& cfg)
{
// The problem is that the schema being validated is that of the schema!!!
return root.find_tag(value_, root, cfg) != nullptr;
}
} // namespace schema_validation{

View file

@ -44,7 +44,7 @@ public:
* Initializes validator from file.
* Throws abstract_validator::error if any error.
*/
schema_validator(const std::string& filename);
schema_validator(const std::string& filename, bool validate_schema = false);
void set_create_exceptions(bool value)
{
@ -75,7 +75,7 @@ private:
typedef std::stack<cnt_map> cnt_stack;
protected:
enum message_type{ WRONG_TAG, EXTRA_TAG, MISSING_TAG, EXTRA_KEY, MISSING_KEY, WRONG_VALUE, WRONG_TYPE };
enum message_type{ WRONG_TAG, EXTRA_TAG, MISSING_TAG, EXTRA_KEY, MISSING_KEY, WRONG_VALUE, WRONG_TYPE, WRONG_PATH };
private:
// error_cache
@ -137,6 +137,8 @@ private:
/** Type validators. */
class_type::map types_;
bool validate_schema_;
protected:
void queue_message(const config& cfg, message_type t, const std::string& file, int line = 0, int n = 0, const std::string& tag = "", const std::string& key = "", const std::string& value = "");
const class_tag& active_tag() const;
@ -145,4 +147,39 @@ protected:
bool is_valid() const {return config_read_;}
class_type::ptr find_type(const std::string& type) const;
};
// A validator specifically designed for validating a schema
// In addition to the usual, it verifies that references within the schema are valid, for example via the [link] tag
class schema_self_validator : public schema_validator
{
public:
schema_self_validator();
virtual void open_tag(const std::string& name, const config& parent, int start_line = 0, const std::string& file = "", bool addition = false) override;
virtual void close_tag() override;
virtual void validate(const config& cfg, const std::string& name, int start_line, const std::string& file) override;
virtual void validate_key(const config& cfg, const std::string& name, const std::string& value, int start_line, const std::string& file) override;
private:
struct reference {
reference(const std::string& value, const std::string& file, int line, const std::string& tag)
: value_(value)
, file_(file)
, tag_(tag)
, line_(line)
{}
std::string value_, file_, tag_;
int line_;
bool match(const std::set<std::string>& with);
bool can_find(const class_tag& root, const config& cfg);
bool operator<(const reference& other);
};
std::string current_path() const;
std::set<std::string> defined_types_, defined_tag_paths_;
std::vector<reference> referenced_types_, referenced_tag_paths_;
std::stack<std::string> tag_stack_;
std::map<std::string, std::string> links_;
std::multimap<std::string, std::string> derivations_;
int type_nesting_, condition_nesting_;
bool tag_path_exists(const config& cfg, const reference& ref);
};
} // namespace schema_validation{