1
0
forked from 0ad/0ad

# Initial support for automatic validation of entity template XML.

Add RelaxNG schemas for all current components.
Add -dumpSchema command-line option to dump the combined entity schema.
Add a Perl script to validate entity templates against the schema.
See #413.

This was SVN commit r7452.
This commit is contained in:
Ykkrosh 2010-04-09 19:02:39 +00:00
parent 336817a849
commit 40688ec5df
23 changed files with 518 additions and 44 deletions

View File

@ -1,5 +1,16 @@
function Armour() {}
Armour.prototype.Schema =
"<element name='Hack'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Pierce'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Crush'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>";
Armour.prototype.Init = function()
{
};
@ -7,10 +18,9 @@ Armour.prototype.Init = function()
Armour.prototype.TakeDamage = function(hack, pierce, crush)
{
// Adjust damage values based on armour
// (Default armour values to 0 if undefined)
var adjHack = Math.max(0, hack - (this.template.Hack || 0));
var adjPierce = Math.max(0, pierce - (this.template.Pierce || 0));
var adjCrush = Math.max(0, crush - (this.template.Crush || 0));
var adjHack = Math.max(0, hack - this.template.Hack);
var adjPierce = Math.max(0, pierce - this.template.Pierce);
var adjCrush = Math.max(0, crush - this.template.Crush);
// Total is sum of individual damages, with minimum damage 1
var total = Math.max(1, adjHack + adjPierce + adjCrush);
@ -22,11 +32,11 @@ Armour.prototype.TakeDamage = function(hack, pierce, crush)
Armour.prototype.GetArmourStrengths = function()
{
// Convert attack values to numbers, default 0 if unspecified
// Convert attack values to numbers
return {
hack: +(this.template.Hack || 0),
pierce: +(this.template.Pierce || 0),
crush: +(this.template.Crush || 0)
hack: +this.template.Hack,
pierce: +this.template.Pierce,
crush: +this.template.Crush
};
};

View File

@ -1,5 +1,39 @@
function Attack() {}
Attack.prototype.Schema =
"<element name='Hack'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Pierce'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Crush'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Range'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<optional>" +
"<element name='MinRange'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='PrepareTime'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='RepeatTime'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='ProjectileSpeed'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</optional>";
Attack.prototype.Init = function()
{
};

View File

@ -1,5 +1,14 @@
function Builder() {}
Builder.prototype.Schema =
"<element name='Entities'>" +
"<attribute name='datatype'><value>tokens</value></attribute>" +
"<text/>" +
"</element>" +
"<element name='Rate'>" +
"<ref name='positiveDecimal'/>" +
"</element>";
Builder.prototype.Init = function()
{
};

View File

@ -1,5 +1,30 @@
function Cost() {}
Cost.prototype.Schema =
"<optional>" +
"<element name='Population'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='PopulationBonus'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='BuildTime'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"</optional>" +
"<element name='Resources'>" +
"<interleave>" +
"<element name='food'><data type='nonNegativeInteger'/></element>" +
"<element name='wood'><data type='nonNegativeInteger'/></element>" +
"<element name='stone'><data type='nonNegativeInteger'/></element>" +
"<element name='metal'><data type='nonNegativeInteger'/></element>" +
"</interleave>" +
"</element>";
Cost.prototype.Init = function()
{
};
@ -26,10 +51,10 @@ Cost.prototype.GetBuildTime = function()
Cost.prototype.GetResourceCosts = function()
{
return {
"food": +(this.template.Resources.food || 0),
"wood": +(this.template.Resources.wood || 0),
"stone": +(this.template.Resources.stone || 0),
"metal": +(this.template.Resources.metal || 0)
"food": +this.template.Resources.food,
"wood": +this.template.Resources.wood,
"stone": +this.template.Resources.stone,
"metal": +this.template.Resources.metal
};
};

View File

@ -1,5 +1,25 @@
function Health() {}
Health.prototype.Schema =
"<element name='Max'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"<optional>" +
"<element name='Initial'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='RegenRate'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='DeathType'>" +
"<value>corpse</value>" +
"</element>" +
"</optional>";
Health.prototype.Init = function()
{
// Default to <Initial>, but use <Max> if it's undefined or zero

View File

@ -1,5 +1,24 @@
function Identity() {}
Identity.prototype.Schema =
"<element name='Civ'>" +
"<text/>" + // TODO: <choice>?
"</element>" +
"<element name='GenericName'>" +
"<text/>" +
"</element>" +
"<optional>" +
"<element name='SpecificName'>" +
"<text/>" +
"</element>" +
"</optional>" +
"<element name='IconCell'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"<element name='IconSheet'>" +
"<text/>" +
"</element>";
Identity.prototype.Init = function()
{
};

View File

@ -1,5 +1,23 @@
function ResourceGatherer() {}
ResourceGatherer.prototype.Schema =
"<element name='Rates'>" +
"<interleave>" +
"<optional><element name='food'><data type='decimal'/></element></optional>" +
"<optional><element name='wood'><data type='decimal'/></element></optional>" +
"<optional><element name='stone'><data type='decimal'/></element></optional>" +
"<optional><element name='metal'><data type='decimal'/></element></optional>" +
"<optional><element name='food.fish'><data type='decimal'/></element></optional>" +
"<optional><element name='food.fruit'><data type='decimal'/></element></optional>" +
"<optional><element name='food.grain'><data type='decimal'/></element></optional>" +
"<optional><element name='food.meat'><data type='decimal'/></element></optional>" +
"<optional><element name='food.milk'><data type='decimal'/></element></optional>" +
"</interleave>" +
"</element>" +
"<element name='BaseSpeed'>" +
"<data type='positiveInteger'/>" +
"</element>";
ResourceGatherer.prototype.Init = function()
{
};

View File

@ -1,5 +1,35 @@
function ResourceSupply() {}
ResourceSupply.prototype.Schema =
"<element name='Amount'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"<choice>" +
"<interleave>" +
"<element name='Type'><value>food</value></element>" +
"<element name='Subtype'><value>fish</value></element>" +
"</interleave>" +
"<interleave>" +
"<element name='Type'><value>food</value></element>" +
"<element name='Subtype'><value>fruit</value></element>" +
"</interleave>" +
"<interleave>" +
"<element name='Type'><value>food</value></element>" +
"<element name='Subtype'><value>grain</value></element>" +
"</interleave>" +
"<interleave>" +
"<element name='Type'><value>food</value></element>" +
"<element name='Subtype'><value>meat</value></element>" +
"</interleave>" +
"<interleave>" +
"<element name='Type'><value>food</value></element>" +
"<element name='Subtype'><value>milk</value></element>" +
"</interleave>" +
"<element name='Type'><value>wood</value></element>" +
"<element name='Type'><value>stone</value></element>" +
"<element name='Type'><value>metal</value></element>" +
"</choice>";
ResourceSupply.prototype.Init = function()
{
// Current resource amount (non-negative; can be a fractional amount)

View File

@ -1,5 +1,15 @@
function Sound() {}
Sound.prototype.Schema =
"<element name='SoundGroups'>" +
"<zeroOrMore>" +
"<element>" +
"<anyName/>" +
"<text/>" +
"</element>" +
"</zeroOrMore>" +
"</element>";
Sound.prototype.Init = function()
{
};

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -856,6 +856,19 @@ void Init(const CmdLineArgs& args, int flags)
// (required for finding our output log files).
g_Logger = new CLogger;
// Special command-line mode to dump the entity schemas instead of running the game.
// (This must be done after loading VFS etc, but should be done before wasting time
// on anything else.)
if (args.Has("dumpSchema"))
{
CSimulation2 sim(NULL, NULL);
sim.LoadDefaultScripts();
std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
f << sim.GenerateSchema();
std::cout << "Generated entity.rng\n";
exit(0);
}
// Call LoadLanguage(NULL) to initialize the I18n system, but
// without loading an actual language file - translate() will
// just show the English key text, which is better than crashing

View File

@ -306,3 +306,8 @@ bool CSimulation2::DeserializeState(std::istream& stream)
// TODO: need to make sure the required SYSTEM_ENTITY components get constructed
return m->m_ComponentManager.DeserializeState(stream);
}
std::string CSimulation2::GenerateSchema()
{
return m->m_ComponentManager.GenerateSchema();
}

View File

@ -128,6 +128,8 @@ public:
bool SerializeState(std::ostream& stream);
bool DeserializeState(std::istream& stream);
std::string GenerateSchema();
private:
CSimulation2Impl* m;

View File

@ -36,6 +36,23 @@ public:
CFixed_23_8 m_Size1; // height/radius
CFixed_23_8 m_Height;
static std::string GetSchema()
{
return
"<choice>"
"<element name='Square'>"
"<attribute name='width'><ref name='positiveDecimal'/></attribute>"
"<attribute name='depth'><ref name='positiveDecimal'/></attribute>"
"</element>"
"<element name='Circle'>"
"<attribute name='radius'><ref name='positiveDecimal'/></attribute>"
"</element>"
"</choice>"
"<element name='Height'>"
"<ref name='nonNegativeDecimal'/>"
"</element>";
}
virtual void Init(const CSimContext& UNUSED(context), const CParamNode& paramNode)
{
if (paramNode.GetChild("Square").IsOk())

View File

@ -46,6 +46,13 @@ public:
ICmpObstructionManager::tag_t m_Tag;
static std::string GetSchema()
{
return
"<optional>"
"<element name='Inactive'><empty/></element>"
"</optional>";
}
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_Context = &context;

View File

@ -75,27 +75,24 @@ public:
bool m_Dirty; // true if position/rotation has changed since last TurnStart
/*
* Schema: (untested)
*
* <element name="Position">
* <interleave>
* <element name="Anchor" a:help="Automatic rotation to follow the slope of terrain">
* <choice>
* <value a:help="Always stand straight up">upright</value>
* <value a:help="Rotate backwards and forwards to follow the terrain">pitch</value>
* <value a:help="Rotate in all direction to follow the terrain">pitch-roll</value>
* </choice>
* </element>
* <element name="Altitude" a:help="Height above terrain in metres">
* <data type="float"/>
* </element>
* <element name="Floating" a:help="Whether the entity floats on water">
* <data type="boolean"/>
* </element>
* </interleave>
* </element>
*/
static std::string GetSchema()
{
return
"<element name='Anchor' a:help='Automatic rotation to follow the slope of terrain'>"
"<choice>"
"<value a:help='Always stand straight up'>upright</value>"
"<value a:help='Rotate backwards and forwards to follow the terrain'>pitch</value>"
"<value a:help='Rotate in all direction to follow the terrain'>pitch-roll</value>"
"</choice>"
"</element>"
"<element name='Altitude' a:help='Height above terrain in metres'>"
"<data type='decimal'/>"
"</element>"
"<element name='Floating' a:help='Whether the entity floats on water'>"
"<data type='boolean'/>"
"</element>";
}
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_Context = &context;

View File

@ -56,6 +56,13 @@ public:
};
int m_State;
static std::string GetSchema()
{
return
"<element name='WalkSpeed'>"
"<ref name='positiveDecimal'/>"
"</element>";
}
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_Context = &context;

View File

@ -60,6 +60,17 @@ public:
{
}
static std::string GetSchema()
{
return
"<optional>"
"<element name='Foundation'><empty/></element>"
"</optional>"
"<optional>"
"<element name='FoundationActor'><text/></element>"
"</optional>"
"<element name='Actor'><text/></element>";
}
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
if (!context.HasUnitManager())
@ -68,7 +79,7 @@ public:
// TODO: we should do some fancy animation of under-construction buildings rising from the ground,
// but for now we'll just use the foundation actor and ignore the normal one
std::string name;
if (paramNode.GetChild("Foundation").IsOk())
if (paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk())
name = utf8_from_wstring(paramNode.GetChild("FoundationActor").ToString());
else
name = utf8_from_wstring(paramNode.GetChild("Actor").ToString());

View File

@ -30,14 +30,14 @@
#define REGISTER_COMPONENT_TYPE(cname) \
void RegisterComponentType_##cname(CComponentManager& mgr) \
{ \
mgr.RegisterComponentType(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname); \
mgr.RegisterComponentType(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \
CCmp##cname::ClassInit(mgr); \
}
#define REGISTER_COMPONENT_SCRIPT_WRAPPER(cname) \
void RegisterComponentType_##cname(CComponentManager& mgr) \
{ \
mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname); \
mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \
CCmp##cname::ClassInit(mgr); \
}

View File

@ -185,8 +185,18 @@ void CComponentManager::Script_RegisterComponentType(void* cbdata, int iid, std:
mustReloadComponents = true;
}
std::string schema = "<empty/>";
{
CScriptValRooted prototype;
if (componentManager->m_ScriptInterface.GetProperty(ctor.get(), "prototype", prototype) &&
componentManager->m_ScriptInterface.HasProperty(prototype.get(), "Schema"))
{
componentManager->m_ScriptInterface.GetProperty(prototype.get(), "Schema", schema);
}
}
// Construct a new ComponentType, using the wrapper's alloc functions
ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, ctor.get() };
ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, ctor.get() };
componentManager->m_ComponentTypesById[cid] = ct;
componentManager->m_CurrentComponent = cid; // needed by Subscribe
@ -365,17 +375,17 @@ void CComponentManager::ResetState()
}
void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc,
const char* name)
const char* name, const std::string& schema)
{
ComponentType c = { CT_Native, iid, alloc, dealloc, name, 0 };
ComponentType c = { CT_Native, iid, alloc, dealloc, name, schema, 0 };
m_ComponentTypesById.insert(std::make_pair(cid, c));
m_ComponentTypeIdsByName[name] = cid;
}
void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc,
DeallocFunc dealloc, const char* name)
DeallocFunc dealloc, const char* name, const std::string& schema)
{
ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, 0 };
ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, schema, 0 };
m_ComponentTypesById.insert(std::make_pair(cid, c));
m_ComponentTypeIdsByName[name] = cid;
// TODO: merge with RegisterComponentType
@ -715,3 +725,68 @@ void CComponentManager::SendGlobalMessage(const CMessage& msg) const
}
}
}
std::string CComponentManager::GenerateSchema()
{
std::string schema =
"<grammar xmlns='http://relaxng.org/ns/structure/1.0' xmlns:a='http://ns.wildfiregames.com/entity' datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>"
"<define name='nonNegativeDecimal'>"
"<data type='decimal'><param name='minInclusive'>0</param></data>"
"</define>"
"<define name='positiveDecimal'>"
"<data type='decimal'><param name='minExclusive'>0</param></data>"
"</define>"
"<define name='anything'>"
"<zeroOrMore>"
"<choice>"
"<attribute><anyName/></attribute>"
"<text/>"
"<element>"
"<anyName/>"
"<ref name='anything'/>"
"</element>"
"</choice>"
"</zeroOrMore>"
"</define>";
std::map<InterfaceId, std::vector<std::string> > interfaceComponentTypes;
for (std::map<ComponentTypeId, ComponentType>::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it)
{
schema +=
"<define name='component." + it->second.name + "'>"
"<element name='" + it->second.name + "'>"
"<interleave>" + it->second.schema + "</interleave>"
"</element>"
"</define>";
interfaceComponentTypes[it->second.iid].push_back(it->second.name);
}
for (std::map<std::string, InterfaceId>::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it)
{
schema += "<define name='interface." + it->first + "'><choice>";
std::vector<std::string>& cts = interfaceComponentTypes[it->second];
for (size_t i = 0; i < cts.size(); ++i)
schema += "<ref name='component." + cts[i] + "'/>";
schema += "</choice></define>";
}
schema +=
"<start>"
"<element name='Entity'>"
"<optional><attribute name='parent'/></optional>"
"<interleave>";
for (std::map<std::string, InterfaceId>::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it)
schema += "<optional><ref name='interface." + it->first + "'/></optional>";
schema +=
"</interleave>"
"</element>"
"</start>";
schema += "</grammar>";
// TODO: pretty-print
return schema;
}

View File

@ -63,6 +63,7 @@ private:
AllocFunc alloc;
DeallocFunc dealloc;
std::string name;
std::string schema; // RelaxNG fragment
jsval ctor; // only valid if type == CT_Script
};
@ -82,8 +83,8 @@ public:
void RegisterMessageType(MessageTypeId mtid, const char* name);
void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*);
void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*);
void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema);
void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema);
void SubscribeToMessageType(MessageTypeId);
void SubscribeGloballyToMessageType(MessageTypeId);
@ -191,6 +192,8 @@ public:
bool SerializeState(std::ostream& stream);
bool DeserializeState(std::istream& stream);
std::string GenerateSchema();
ScriptInterface& GetScriptInterface() { return m_ScriptInterface; }
private:

View File

@ -23,6 +23,12 @@ IComponent::~IComponent()
{
}
std::string IComponent::GetSchema()
{
// No schema specified -> allow only empty elements
return "<empty/>";
}
void IComponent::HandleMessage(const CSimContext& UNUSED(context), const CMessage& UNUSED(msg), bool UNUSED(global))
{
}

View File

@ -35,6 +35,8 @@ class IComponent
public:
virtual ~IComponent();
static std::string GetSchema();
virtual void Init(const CSimContext& context, const CParamNode& paramNode) = 0;
virtual void Deinit(const CSimContext& context) = 0;

View File

@ -0,0 +1,154 @@
use strict;
use warnings;
use XML::Parser;
use XML::LibXML;
use Data::Dumper;
use Storable qw(dclone);
use File::Find;
my $root = '../../../binaries/data/mods/public/simulation/templates';
my $rngschema = XML::LibXML::RelaxNG->new(location =>'../../../binaries/system/entity.rng');
sub get_file
{
my ($vfspath) = @_;
my $fn = "$root/$vfspath.xml";
open my $f, $fn or die "Error loading $fn: $!";
local $/;
return <$f>;
}
sub trim
{
my ($t) = @_;
return '' if not defined $t;
$t =~ /^\s*(.*?)\s*$/s;
return $1;
}
sub load_xml
{
my ($file) = @_;
my $root = {};
my @stack = ($root);
my $p = new XML::Parser(Handlers => {
Start => sub {
my ($e, $n, %a) = @_;
my $t = {};
die "Duplicate child node '$n'" if exists $stack[-1]{$n};
$stack[-1]{$n} = $t;
for (keys %a) {
$t->{'@'.$_}{' content'} = trim($a{$_});
}
push @stack, $t;
},
End => sub {
my ($e, $n) = @_;
$stack[-1]{' content'} = trim($stack[-1]{' content'});
pop @stack;
},
Char => sub {
my ($e, $str) = @_;
$stack[-1]{' content'} .= $str;
},
});
$p->parse($file);
return $root;
}
sub apply_layer
{
my ($base, $new) = @_;
$base->{' content'} = $new->{' content'};
for my $k (grep $_ ne ' content', keys %$new) {
if ($new->{$k}{'@disable'}) {
delete $base->{$k};
} else {
if ($new->{$k}{'@replace'}) {
delete $base->{$k};
}
$base->{$k} ||= {};
apply_layer($base->{$k}, $new->{$k});
delete $base->{$k}{'@replace'};
}
}
}
sub load_inherited
{
my ($vfspath) = @_;
my $layer = load_xml(get_file($vfspath));
if ($layer->{Entity}{'@parent'}) {
my $parent = load_inherited($layer->{Entity}{'@parent'}{' content'});
apply_layer($parent->{Entity}, $layer->{Entity});
return $parent;
} else {
return $layer;
}
}
sub escape_xml
{
my ($t) = @_;
$t =~ s/&/&amp;/g;
$t =~ s/</&lt;/g;
$t =~ s/>/&gt;/g;
$t =~ s/"/&quot;/g;
$t =~ s/\t/&#9;/g;
$t =~ s/\n/&#10;/g;
$t =~ s/\r/&#13;/g;
$t;
}
sub to_xml
{
my ($e) = @_;
my $r = $e->{' content'};
$r = '' if not defined $r;
for my $k (sort grep !/^[\@ ]/, keys %$e) {
$r .= "<$k";
for my $a (sort grep /^\@/, keys %{$e->{$k}}) {
$a =~ /^\@(.*)/;
$r .= " $1=\"".escape_xml($e->{$k}{$a}{' content'})."\"";
}
$r .= ">";
$r .= to_xml($e->{$k});
$r .= "</$k>";
}
return $r;
}
sub validate
{
my ($vfspath) = @_;
my $xml = to_xml(load_inherited($vfspath));
my $doc = XML::LibXML->new->parse_string($xml);
$rngschema->validate($doc);
}
my @files;
sub find_process {
return $File::Find::prune = 1 if $_ eq '.svn';
my $n = $File::Find::name;
return if /~$/;
return unless -f $_;
$n =~ s/\Q$root\///;
$n =~ s/\.xml$//;
push @files, $n;
}
find({ wanted => \&find_process }, $root);
for my $f (sort @files) {
next if $f =~ /^template_/;
print "# $f...\n";
eval {
validate($f);
};
if ($@) {
print $@;
eval { print to_xml(load_inherited($f)), "\n"; }
}
}