载入中...
搜索中...
未找到
EditorAssetDatabase.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cctype>
5#include <set>
6
7namespace eve::editor {
8namespace {
9
10bool containsInsensitive(std::string value, std::string text) {
11 auto lower = [](unsigned char character) { return static_cast<char>(std::tolower(character)); };
12 std::transform(value.begin(), value.end(), value.begin(), lower);
13 std::transform(text.begin(), text.end(), text.begin(), lower);
14 return value.find(text) != std::string::npos;
15}
16
17} // namespace
18
19Result<AssetRecord> MemoryAssetDatabase::publish(AssetRecord record, std::vector<AssetDependency> dependencies) {
20 if (record.guid.empty() || record.logicalUri.empty() || record.typeId.empty())
21 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.asset.invalid-record"),
22 "Asset GUID, logical URI and type are required");
23 const auto uriOwner = uriToGuid_.find(record.logicalUri);
24 if (uriOwner != uriToGuid_.end() && uriOwner->second != record.guid)
25 return eve::editing::failed<AssetRecord>(EditorStatus::Conflict, RuleId("editor.asset.uri-conflict"),
26 "Another asset already owns this logical URI");
27 for (const AssetDependency& dependency : dependencies) {
28 if (dependency.from != record.guid || dependency.to.empty())
29 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.asset.invalid-dependency"),
30 "Published dependencies must originate from the product asset");
31 }
33 const auto existing = records_.find(record.guid);
34 if (existing != records_.end() && existing->second.logicalUri != record.logicalUri)
35 uriToGuid_.erase(existing->second.logicalUri);
36 uriToGuid_.insert_or_assign(record.logicalUri, record.guid);
37 records_.insert_or_assign(record.guid, record);
38 std::erase_if(dependencies_, [&](const AssetDependency& dependency) { return dependency.from == record.guid; });
39 dependencies_.insert(dependencies_.end(), dependencies.begin(), dependencies.end());
40 ++generation_;
41 return eve::editing::applied<AssetRecord>(std::move(record));
42}
43
45 std::vector<AssetPublication> publications) {
46 if (publications.empty())
47 return eve::editing::failed<std::vector<AssetRecord>>(EditorStatus::Rejected,
48 RuleId("editor.asset.empty-publication"),
49 "An asset publication batch must not be empty");
50 auto stagedRecords = records_;
51 auto stagedUris = uriToGuid_;
52 auto stagedDependencies = dependencies_;
53 std::vector<AssetRecord> published;
54 std::unordered_map<AssetGuid, bool, StrongEditorIdHash<AssetGuid>> batchGuids;
55 published.reserve(publications.size());
56 for (AssetPublication& publication : publications) {
57 AssetRecord& record = publication.record;
58 if (record.guid.empty() || record.logicalUri.empty() || record.typeId.empty())
59 return eve::editing::failed<std::vector<AssetRecord>>(EditorStatus::Rejected,
60 RuleId("editor.asset.invalid-record"),
61 "Asset GUID, logical URI and type are required");
62 if (!batchGuids.emplace(record.guid, true).second)
63 return eve::editing::failed<std::vector<AssetRecord>>(
64 EditorStatus::Conflict, RuleId("editor.asset.duplicate-batch-guid"),
65 "An asset publication batch contains a duplicate GUID");
66 const auto uriOwner = stagedUris.find(record.logicalUri);
67 if (uriOwner != stagedUris.end() && uriOwner->second != record.guid)
68 return eve::editing::failed<std::vector<AssetRecord>>(EditorStatus::Conflict,
69 RuleId("editor.asset.uri-conflict"),
70 "Another asset already owns this logical URI");
71 for (const AssetDependency& dependency : publication.dependencies) {
72 if (dependency.from != record.guid || dependency.to.empty())
73 return eve::editing::failed<std::vector<AssetRecord>>(
74 EditorStatus::Rejected, RuleId("editor.asset.invalid-dependency"),
75 "Published dependencies must originate from the product asset");
76 }
78 const auto existing = stagedRecords.find(record.guid);
79 if (existing != stagedRecords.end() && existing->second.logicalUri != record.logicalUri)
80 stagedUris.erase(existing->second.logicalUri);
81 stagedUris.insert_or_assign(record.logicalUri, record.guid);
82 stagedRecords.insert_or_assign(record.guid, record);
83 std::erase_if(stagedDependencies,
84 [&](const AssetDependency& dependency) { return dependency.from == record.guid; });
85 stagedDependencies.insert(stagedDependencies.end(), publication.dependencies.begin(),
86 publication.dependencies.end());
87 published.push_back(std::move(record));
88 }
89 records_ = std::move(stagedRecords);
90 uriToGuid_ = std::move(stagedUris);
91 dependencies_ = std::move(stagedDependencies);
92 ++generation_;
93 return eve::editing::applied<std::vector<AssetRecord>>(std::move(published));
94}
95
97 auto found = records_.find(guid);
98 if (found == records_.end())
99 return eve::editing::failed<AssetRecord>(EditorStatus::NotFound, RuleId("editor.asset.not-found"),
100 "Asset is not indexed: " + guid.value());
101 return eve::editing::applied<AssetRecord>(found->second);
102}
103
104Result<AssetRecord> MemoryAssetDatabase::findByUri(const std::string& logicalUri) const {
105 const auto guid = uriToGuid_.find(logicalUri);
106 if (guid != uriToGuid_.end()) return find(guid->second);
107 return eve::editing::failed<AssetRecord>(EditorStatus::NotFound, RuleId("editor.asset.not-found"),
108 "Asset URI is not indexed: " + logicalUri);
109}
110
112 std::size_t limit,
113 std::optional<std::uint64_t> generation) const {
114 if (generation && *generation != generation_)
115 return eve::editing::failed<AssetPage<AssetRecord>>(EditorStatus::Conflict, RuleId("editor.asset.page-expired"),
116 "Asset index changed while paging");
117 std::vector<AssetRecord> matches;
118 for (const auto& [guid, record] : records_) {
119 (void)guid;
120 if (query.status && record.status != *query.status) continue;
121 if (!query.typeIds.empty() &&
122 std::find(query.typeIds.begin(), query.typeIds.end(), record.typeId) == query.typeIds.end())
123 continue;
124 if (!query.pathPrefixes.empty()) {
125 bool prefix = false;
126 for (const std::string& candidate : query.pathPrefixes)
127 if (record.logicalUri.starts_with(candidate)) prefix = true;
128 if (!prefix) continue;
129 }
130 bool tags = true;
131 for (const std::string& tag : query.tagsAll)
132 if (std::find(record.tags.begin(), record.tags.end(), tag) == record.tags.end()) tags = false;
133 if (!tags) continue;
134 if (!query.text.empty() && !containsInsensitive(record.logicalUri, query.text) &&
135 !containsInsensitive(record.typeId, query.text))
136 continue;
137 matches.push_back(record);
138 }
139 std::sort(matches.begin(), matches.end(),
140 [](const AssetRecord& left, const AssetRecord& right) { return left.logicalUri < right.logicalUri; });
142 page.generation = generation_;
143 if (offset < matches.size()) {
144 const std::size_t end = std::min(matches.size(), offset + limit);
145 page.values.assign(matches.begin() + static_cast<std::ptrdiff_t>(offset),
146 matches.begin() + static_cast<std::ptrdiff_t>(end));
147 page.nextOffset = end;
148 page.hasMore = end < matches.size();
149 }
150 return eve::editing::applied<AssetPage<AssetRecord>>(std::move(page));
151}
152
153std::vector<AssetDependency> MemoryAssetDatabase::dependencies(const AssetGuid& guid, bool incoming) const {
154 std::vector<AssetDependency> result;
155 for (const AssetDependency& dependency : dependencies_)
156 if ((incoming && dependency.to == guid) || (!incoming && dependency.from == guid)) result.push_back(dependency);
157 return result;
158}
159
161 if (!database_)
162 return eve::editing::failed<AssetRecord>(EditorStatus::Failed, RuleId("editor.import.missing-database"),
163 "Import coordinator has no asset database");
164 if (product.record.sourceUri.empty() || product.record.importerId.empty())
165 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.import.invalid-product"),
166 "Import product requires source URI and importer identity");
167 return database_->publish(std::move(product.record), std::move(product.dependencies));
168}
169
170Result<ImportTicket> ImportCoordinator::begin(const AssetGuid& asset, std::string sourceHash,
171 std::string importerId,
172 std::uint32_t importerVersion) {
173 if (asset.empty() || sourceHash.empty() || importerId.empty() || importerVersion == 0)
174 return eve::editing::failed<ImportTicket>(EditorStatus::Rejected, RuleId("editor.import.invalid-request"),
175 "Import request requires asset, source hash and versioned importer");
176 const std::uint64_t generation = ++generations_[asset];
177 return eve::editing::applied<ImportTicket>(
178 {asset, generation, std::move(sourceHash), std::move(importerId), importerVersion});
179}
180
182 ImportProduct product) {
183 if (!database_)
184 return eve::editing::failed<AssetRecord>(EditorStatus::Failed, RuleId("editor.import.missing-database"),
185 "Import coordinator has no asset database");
186 const auto generation = generations_.find(ticket.asset);
187 if (generation == generations_.end() || generation->second != ticket.generation)
188 return eve::editing::failed<AssetRecord>(EditorStatus::Conflict, RuleId("editor.import.stale-product"),
189 "Importer product was superseded by a newer source generation");
190 AssetRecord& record = product.record;
191 if (record.guid != ticket.asset || record.sourceHash != ticket.sourceHash ||
192 record.importerId != ticket.importerId || record.importerVersion != ticket.importerVersion)
193 return eve::editing::failed<AssetRecord>(EditorStatus::Conflict, RuleId("editor.import.ticket-mismatch"),
194 "Importer product identity does not match its request ticket");
195 if (record.sourceUri.empty() || record.logicalUri.empty() || record.typeId.empty())
196 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.import.incomplete-product"),
197 "Importer product lacks source, logical URI or asset type");
198 std::set<std::string> artifacts;
199 for (const std::string& artifact : record.artifacts)
200 if (artifact.empty() || !artifacts.insert(artifact).second)
201 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.import.invalid-artifact"),
202 "Importer artifacts must be unique and non-empty");
203 if (record.artifacts.empty())
204 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.import.missing-artifact"),
205 "Importer product must contain at least one published artifact");
206 if (std::any_of(record.diagnostics.begin(), record.diagnostics.end(),
207 [](const EditorDiagnostic& diagnostic) {
208 return diagnostic.severity() == DiagnosticSeverity::Error ||
209 diagnostic.severity() == DiagnosticSeverity::Fatal;
210 }))
211 return eve::editing::failed<AssetRecord>(EditorStatus::Rejected, RuleId("editor.import.product-errors"),
212 "Importer product contains error diagnostics");
213 auto published = database_->publish(std::move(record), std::move(product.dependencies));
214 if (published.ok()) generations_.erase(ticket.asset);
215 return published;
216}
217
218} // namespace eve::editor
double value
HexVec3 left
HexVec3 right
std::string text
size_t offset
const std::string * tag
std::uint32_t generation
bool found
int limit
Definition TreeMesh.cpp:164
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
Move-only operation result carrying either a value or Status.
Definition Result.h:155
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
bool empty() const noexcept
True when no compatibility spelling has been assigned.
Definition Identity.h:374
Result< AssetRecord > publish(ImportProduct product)
Validate and publish a completed import product.
Result< ImportTicket > begin(const AssetGuid &asset, std::string sourceHash, std::string importerId, std::uint32_t importerVersion)
Start or supersede one asset import and return its immutable generation ticket.
Result< AssetRecord > find(const AssetGuid &guid) const
Find one asset by stable GUID.
Result< AssetRecord > findByUri(const std::string &logicalUri) const
Find one asset by logical content URI.
Result< AssetPage< AssetRecord > > query(const AssetQuery &query, std::size_t offset, std::size_t limit, std::optional< std::uint64_t > generation=std::nullopt) const
Query a deterministic page; stale generations return Conflict.
Result< std::vector< AssetRecord > > publishBatch(std::vector< AssetPublication > publications)
Validate and atomically publish an entire package projection as one index generation.
std::uint64_t generation() const
Return the monotonic index generation.
std::vector< AssetDependency > dependencies(const AssetGuid &guid, bool incoming=false) const
Query outgoing or incoming dependencies for one asset.
Result< AssetRecord > publish(AssetRecord record, std::vector< AssetDependency > dependencies={})
Atomically publish one validated record and its dependencies.
Result< T > failed(Status status, eve::DiagnosticCode code, RuleId rule, std::string message)
Construct a failed editing result with explicit status and diagnostic categories.
eve::editing::RuleId RuleId
Definition EditorIds.h:32
Directed dependency between two stable asset identities.
One page of values tied to an asset index generation.
One owning record/dependency unit staged for an atomic index generation.
Deterministic filter used by headless and UI asset queries.
Value snapshot stored by the editor asset index.
std::vector< std::string > artifacts
std::vector< EditorDiagnostic > diagnostics
Validated importer output staged before publishing to AssetDB.
std::vector< AssetDependency > dependencies
Generation-qualified import request used to reject stale worker products.