载入中...
搜索中...
未找到
MapObjectContract.cpp
浏览该文件的文档.
2
3#include "common/Value.h"
4
5#include <cerrno>
6#include <charconv>
7#include <cmath>
8#include <cstdlib>
9#include <map>
10#include <optional>
11#include <set>
12#include <string>
13
14namespace eve::map {
15namespace {
16
17const eve::Value* member(const eve::Value::Object& object, std::string_view name) {
18 const auto found = object.find(std::string(name));
19 return found == object.end() ? nullptr : &found->second;
20}
21
22bool booleanMember(const eve::Value::Object& object, std::string_view name, bool fallback, bool& output) {
23 const eve::Value* value = member(object, name);
24 if (!value) {
25 output = fallback;
26 return true;
27 }
28 if (!value->isBool()) return false;
29 output = value->asBool();
30 return true;
31}
32
33bool numericValue(const eve::Value& value, double& output) {
34 if (value.isInt64()) output = static_cast<double>(value.asInt());
35 else if (value.isDouble()) output = value.asDouble();
36 else return false;
37 return std::isfinite(output);
38}
39
40bool parseInteger(std::string_view text, std::int64_t& output) {
41 if (text.empty()) return false;
42 const auto parsed = std::from_chars(text.data(), text.data() + text.size(), output);
43 return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size();
44}
45
46bool parseNumber(const std::string& text, double& output) {
47 if (text.empty()) return false;
48 char* end = nullptr;
49 errno = 0;
50 output = std::strtod(text.c_str(), &end);
51 return errno != ERANGE && end == text.c_str() + text.size() && std::isfinite(output);
52}
53
54bool stableName(std::string_view name) {
55 if (name.empty() || name.size() > 256) return false;
56 for (unsigned char character : name)
57 if (character < 0x20 || character == 0x7f) return false;
58 return true;
59}
60
61struct PropertyRule {
62 std::string name;
63 std::string kind;
64 bool required = false;
65 std::optional<double> minimum;
66 std::optional<double> maximum;
67 std::set<std::string> choices;
68};
69
70struct TypeRule {
72 std::map<std::string, PropertyRule> properties;
73};
74
75eve::Result<void> validateProperty(const std::string& value, const PropertyRule& rule,
76 const std::string& path) {
77 double numeric = 0.0;
78 if (rule.kind == "string") {
79 if (!rule.choices.empty() && !rule.choices.contains(value))
81 eve::DiagnosticCode::InvalidArgument, "property value is outside its enum", path, {}, "map.object-contract"));
82 } else if (rule.kind == "int") {
83 std::int64_t integer = 0;
84 if (!parseInteger(value, integer))
86 eve::DiagnosticCode::TypeMismatch, "property must be an integer", path, {}, "map.object-contract"));
87 numeric = static_cast<double>(integer);
88 } else if (rule.kind == "number") {
89 if (!parseNumber(value, numeric))
91 eve::DiagnosticCode::TypeMismatch, "property must be a finite number", path, {}, "map.object-contract"));
92 } else if (rule.kind == "bool") {
93 if (value != "true" && value != "false")
95 eve::DiagnosticCode::TypeMismatch, "property must be true or false", path, {}, "map.object-contract"));
96 } else {
98 eve::DiagnosticCode::Unsupported, "unsupported property kind", path, {}, "map.object-contract"));
99 }
100 if (rule.minimum && numeric < *rule.minimum)
102 eve::DiagnosticCode::InvalidArgument, "property is below its minimum", path, {}, "map.object-contract"));
103 if (rule.maximum && numeric > *rule.maximum)
105 eve::DiagnosticCode::InvalidArgument, "property exceeds its maximum", path, {}, "map.object-contract"));
107}
108
109} // namespace
110
111eve::Result<void> validateMapObjects(std::span<const MapObject> objects, std::string_view contractJson) {
112 auto parsed = eve::Value::fromJson(contractJson);
113 if (!parsed.ok()) return eve::Result<void>::failure(parsed.status());
114 const auto* root = parsed.value().getIf<eve::Value::Object>();
116 eve::DiagnosticCode::TypeMismatch, "contract root must be an object", "$contract", {}, "map.object-contract"));
117
118 const eve::Value* schema = member(*root, "schema");
119 const eve::Value* version = member(*root, "version");
120 const eve::Value* typesValue = member(*root, "types");
121 if (!schema || !schema->isString() || schema->asString() != "eve.map.object-contract")
123 eve::DiagnosticCode::InvalidArgument, "unsupported contract schema", "$contract.schema", {}, "map.object-contract"));
124 if (!version || !version->isInt64() || version->asInt() != 1)
126 eve::DiagnosticCode::UnknownVersion, "unsupported contract version", "$contract.version", {}, "map.object-contract"));
127 const auto* types = typesValue ? typesValue->getIf<eve::Value::Array>() : nullptr;
129 eve::DiagnosticCode::TypeMismatch, "types must be an array", "$contract.types", {}, "map.object-contract"));
130
131 bool requireUniqueNames = true;
132 bool allowUnknownTypes = false;
133 if (!booleanMember(*root, "requireUniqueNames", true, requireUniqueNames))
135 eve::DiagnosticCode::TypeMismatch, "requireUniqueNames must be boolean", "$contract.requireUniqueNames", {}, "map.object-contract"));
136 if (!booleanMember(*root, "allowUnknownTypes", false, allowUnknownTypes))
138 eve::DiagnosticCode::TypeMismatch, "allowUnknownTypes must be boolean", "$contract.allowUnknownTypes", {}, "map.object-contract"));
139
140 std::map<std::string, TypeRule> rules;
141 for (std::size_t typeIndex = 0; typeIndex < types->size(); ++typeIndex) {
142 const std::string base = "$contract.types[" + std::to_string(typeIndex) + "]";
143 const auto* definition = (*types)[typeIndex].getIf<eve::Value::Object>();
145 eve::DiagnosticCode::TypeMismatch, "type rule must be an object", base, {}, "map.object-contract"));
146 const eve::Value* type = member(*definition, "type");
147 if (!type || !type->isString() || type->asString().empty())
149 eve::DiagnosticCode::InvalidArgument, "type must be a non-empty string", base + ".type", {}, "map.object-contract"));
150 TypeRule typeRule;
151 if (!booleanMember(*definition, "allowUnknownProperties", false, typeRule.allowUnknownProperties))
153 eve::DiagnosticCode::TypeMismatch, "allowUnknownProperties must be boolean", base + ".allowUnknownProperties", {}, "map.object-contract"));
154 const eve::Value* propertiesValue = member(*definition, "properties");
155 const auto* properties = propertiesValue ? propertiesValue->getIf<eve::Value::Array>() : nullptr;
156 if (propertiesValue && !properties)
158 eve::DiagnosticCode::TypeMismatch, "properties must be an array", base + ".properties", {}, "map.object-contract"));
159 if (properties) {
160 for (std::size_t propertyIndex = 0; propertyIndex < properties->size(); ++propertyIndex) {
161 const std::string propertyPath = base + ".properties[" + std::to_string(propertyIndex) + "]";
162 const auto* definitionObject = (*properties)[propertyIndex].getIf<eve::Value::Object>();
163 if (!definitionObject)
165 eve::DiagnosticCode::TypeMismatch, "property rule must be an object", propertyPath, {}, "map.object-contract"));
166 const eve::Value* name = member(*definitionObject, "name");
167 const eve::Value* kind = member(*definitionObject, "kind");
168 if (!name || !name->isString() || name->asString().empty())
170 eve::DiagnosticCode::InvalidArgument, "property name must be non-empty", propertyPath + ".name", {}, "map.object-contract"));
171 if (!kind || !kind->isString())
173 eve::DiagnosticCode::TypeMismatch, "property kind must be a string", propertyPath + ".kind", {}, "map.object-contract"));
174 PropertyRule propertyRule{name->asString(), kind->asString()};
175 if (!booleanMember(*definitionObject, "required", false, propertyRule.required))
177 eve::DiagnosticCode::TypeMismatch, "required must be boolean", propertyPath + ".required", {}, "map.object-contract"));
178 for (std::string_view field : {std::string_view("min"), std::string_view("max")}) {
179 if (const eve::Value* bound = member(*definitionObject, field)) {
180 double number = 0.0;
181 if (!numericValue(*bound, number))
183 eve::DiagnosticCode::TypeMismatch, std::string(field) + " must be numeric", propertyPath + "." + std::string(field), {}, "map.object-contract"));
184 (field == "min" ? propertyRule.minimum : propertyRule.maximum) = number;
185 }
186 }
187 if (propertyRule.minimum && propertyRule.maximum && *propertyRule.minimum > *propertyRule.maximum)
189 eve::DiagnosticCode::InvalidArgument, "min must not exceed max", propertyPath, {}, "map.object-contract"));
190 if (const eve::Value* enumValue = member(*definitionObject, "enum")) {
191 const auto* choices = enumValue->getIf<eve::Value::Array>();
192 if (!choices)
194 eve::DiagnosticCode::TypeMismatch, "enum must be an array", propertyPath + ".enum", {}, "map.object-contract"));
195 for (const eve::Value& choice : *choices) {
196 if (!choice.isString())
198 eve::DiagnosticCode::TypeMismatch, "enum values must be strings", propertyPath + ".enum", {}, "map.object-contract"));
199 if (!propertyRule.choices.emplace(choice.asString()).second)
201 eve::DiagnosticCode::AlreadyExists, "duplicate enum value", propertyPath + ".enum", {}, "map.object-contract"));
202 }
203 }
204 if (!typeRule.properties.emplace(propertyRule.name, std::move(propertyRule)).second)
206 eve::DiagnosticCode::AlreadyExists, "duplicate property rule", propertyPath + ".name", {}, "map.object-contract"));
207 }
208 }
209 if (!rules.emplace(type->asString(), std::move(typeRule)).second)
211 eve::DiagnosticCode::AlreadyExists, "duplicate type rule", base + ".type", {}, "map.object-contract"));
212 }
213
214 std::set<std::string> names;
215 for (std::size_t objectIndex = 0; objectIndex < objects.size(); ++objectIndex) {
216 const MapObject& object = objects[objectIndex];
217 const std::string base = "$objects[" + std::to_string(objectIndex) + "]";
218 if (!std::isfinite(object.x) || !std::isfinite(object.y) || !std::isfinite(object.width) ||
219 !std::isfinite(object.height) || object.width < 0.f || object.height < 0.f)
221 eve::DiagnosticCode::InvalidArgument, "object geometry must be finite and non-negative", base, {}, "map.object-contract"));
222 if (requireUniqueNames && (!stableName(object.name) || !names.emplace(object.name).second))
224 eve::DiagnosticCode::Conflict, "object names must be unique, bounded, non-empty, and control-free", base + ".name", {}, "map.object-contract"));
225 const auto type = rules.find(object.type);
226 if (type == rules.end()) {
227 if (!allowUnknownTypes)
229 eve::DiagnosticCode::Unsupported, "object type is not admitted", base + ".type", {}, "map.object-contract"));
230 continue;
231 }
232 for (const auto& [name, rule] : type->second.properties) {
233 const auto property = object.properties.find(name);
234 if (property == object.properties.end()) {
235 if (rule.required)
237 eve::DiagnosticCode::NotFound, "required property is missing", base + ".properties." + name, {}, "map.object-contract"));
238 continue;
239 }
240 auto valid = validateProperty(property->second, rule, base + ".properties." + name);
241 if (!valid.ok()) return valid;
242 }
243 if (!type->second.allowUnknownProperties) {
244 for (const auto& [name, value] : object.properties) {
245 (void)value;
246 if (!type->second.properties.contains(name))
248 eve::DiagnosticCode::Unsupported, "property is not admitted", base + ".properties." + name, {}, "map.object-contract"));
249 }
250 }
251 }
253}
254
255} // namespace eve::map
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::string output
float maximum[3]
float minimum[3]
std::uint32_t height
std::uint32_t width
std::string text
TokenKind kind
std::map< std::string, PropertyRule > properties
bool allowUnknownProperties
bool required
std::set< std::string > choices
std::string name
bool valid
std::string path
Definition PlayHost.cpp:110
double number
bool found
float(ui::Theme::* member)[4]
Owning, renderer-independent dynamic values.
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
Definition Value.cpp:87
bool isInt64() const noexcept
Return true when this value is an Int64.
Definition Value.h:91
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
bool isString() const noexcept
Return true when this value is a string.
Definition Value.h:95
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Definition Value.cpp:69
std::vector< Value > Array
Definition Value.h:33
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
放置世界:格子占用(多通道)+ 地形语义 + 已放置建筑实例。 行为由 PlacementSystem 提供;本类暴露便于脚本绑定的薄封装方法。 坐标换算统一走 eve::grid(支持 rectang...
eve::Result< void > validateMapObjects(std::span< const MapObject > objects, std::string_view contractJson)
Validate map objects against an owning JSON content contract.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
A named, typed rectangle object from a Tiled objectgroup layer. Used by Map::setObjects / getObject* ...
Definition MapObject.h:13