载入中...
搜索中...
未找到
EditorCommandService.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <exception>
5#include <utility>
6
7namespace eve::editor {
8
9namespace {
10
11constexpr size_t kMaxPayloadDepth = 32;
12constexpr size_t kMaxPayloadElements = 100'000;
13
14} // namespace
15
16Result<EditorValue> EditorCommandService::error(EditorStatus status, const char* rule, std::string message) {
17 return eve::editing::failed<EditorValue>(status, RuleId(rule), std::move(message));
18}
19
20Result<void> EditorCommandService::checkExecutionPolicy(const CommandDescriptor& descriptor,
22 const EditorValue& payload,
23 const HostProfile& profile) const {
24 if (!profile.allowsCommand(id))
25 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.command.profile-denied"),
26 "Command is unavailable in this host: " + id.value());
27 if (!profile.hasFeatures(descriptor.requiredFeatures))
28 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.command.feature-denied"),
29 "Command requires features unavailable in this host: " + id.value());
30 if (source == CommandSource::Automation && !descriptor.automationAllowed)
31 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.command.automation-denied"),
32 "Command is not available to automation: " + id.value());
33 if (!payload.isWithinLimits(kMaxPayloadDepth, kMaxPayloadElements, profile.maxPayloadBytes()))
34 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.command.payload-limit"),
35 "Command payload exceeds host limits: " + id.value());
36 return eve::editing::applied<void>();
37}
38
40 EditorCommandHandler handler, bool replace) {
41 if (!descriptor.id || descriptor.ownerModule.empty() || !handler)
42 return error(EditorStatus::Rejected, "editor.command.invalid-registration",
43 "Command id, owner module and handler are required");
44
45 auto it = std::find_if(commands_.begin(), commands_.end(),
46 [&](const Registration& entry) { return entry.descriptor.id == descriptor.id; });
47 if (it != commands_.end()) {
48 if (!replace || it->descriptor.ownerModule != descriptor.ownerModule)
49 return error(EditorStatus::Rejected, "editor.command.duplicate",
50 "Command is already registered: " + descriptor.id.value());
51 *it = {std::move(descriptor), std::move(handler), {}, {}, nextRegistrationGeneration_++};
52 } else {
53 commands_.push_back({std::move(descriptor), std::move(handler), {}, {}, nextRegistrationGeneration_++});
54 }
55 ++revision_;
56 return eve::editing::applied<EditorValue>(EditorValue{});
57}
58
62 bool replace) {
63 if (!descriptor.id || descriptor.ownerModule.empty() || !planner || !executor)
64 return error(EditorStatus::Rejected, "editor.command.invalid-registration",
65 "Command id, owner module, planner and executor are required");
66 auto it = std::find_if(commands_.begin(), commands_.end(),
67 [&](const Registration& entry) { return entry.descriptor.id == descriptor.id; });
68 if (it != commands_.end()) {
69 if (!replace || it->descriptor.ownerModule != descriptor.ownerModule)
70 return error(EditorStatus::Rejected, "editor.command.duplicate",
71 "Command is already registered: " + descriptor.id.value());
72 *it = {std::move(descriptor), {}, std::move(planner), std::move(executor),
73 nextRegistrationGeneration_++};
74 } else {
75 commands_.push_back({std::move(descriptor), {}, std::move(planner), std::move(executor),
76 nextRegistrationGeneration_++});
77 }
78 ++revision_;
79 return eve::editing::applied<EditorValue>(EditorValue{});
80}
81
82bool EditorCommandService::unregisterCommand(const CommandId& id, const std::string& ownerModule) {
83 auto it = std::find_if(commands_.begin(), commands_.end(), [&](const Registration& entry) {
84 return entry.descriptor.id == id && (ownerModule.empty() || entry.descriptor.ownerModule == ownerModule);
85 });
86 if (it == commands_.end()) return false;
87 commands_.erase(it);
88 ++revision_;
89 return true;
90}
91
92size_t EditorCommandService::unregisterOwner(const std::string& ownerModule) {
93 if (ownerModule.empty()) return 0;
94 const size_t before = commands_.size();
95 std::erase_if(commands_, [&](const Registration& entry) { return entry.descriptor.ownerModule == ownerModule; });
96 const size_t removed = before - commands_.size();
97 if (removed > 0) ++revision_;
98 return removed;
99}
100
102 if (commands_.empty()) return;
103 commands_.clear();
104 ++revision_;
105}
106
108 auto it = std::find_if(commands_.begin(), commands_.end(),
109 [&](const Registration& entry) { return entry.descriptor.id == id; });
110 return it == commands_.end() ? nullptr : &it->descriptor;
111}
112
114 const auto it = std::find_if(commands_.begin(), commands_.end(),
115 [&](const Registration& entry) { return entry.descriptor.id == id; });
116 return it != commands_.end() && static_cast<bool>(it->planner) && static_cast<bool>(it->planExecutor);
117}
118
119std::vector<CommandDescriptor> EditorCommandService::commands(const HostProfile& profile) const {
120 std::vector<CommandDescriptor> result;
121 result.reserve(commands_.size());
122 for (const auto& entry : commands_)
123 if (profile.allowsCommand(entry.descriptor.id) && profile.hasFeatures(entry.descriptor.requiredFeatures))
124 result.push_back(entry.descriptor);
125 return result;
126}
127
129 const EditorValue& payload) const {
130 auto it = std::find_if(commands_.begin(), commands_.end(),
131 [&](const Registration& entry) { return entry.descriptor.id == id; });
132 if (it == commands_.end())
133 return error(EditorStatus::NotFound, "editor.command.not-found", "Command is not registered: " + id.value());
134 if (!context.profile)
135 return error(EditorStatus::Failed, "editor.command.missing-profile", "Command context has no host profile");
136 if (auto policy = checkExecutionPolicy(it->descriptor, id, context.source, payload, *context.profile);
137 !policy.ok())
138 return Result<EditorValue>::failure(std::move(policy).status());
139
140 if (!it->handler)
141 return error(EditorStatus::Unsupported, "editor.command.requires-plan",
142 "Command must be planned before it can be executed: " + id.value());
143 try {
144 return it->handler(context, payload);
145 } catch (const std::exception& exception) {
146 return error(EditorStatus::Failed, "editor.command.handler-exception",
147 "Command handler failed: " + std::string(exception.what()));
148 } catch (...) {
149 return error(EditorStatus::Failed, "editor.command.handler-exception",
150 "Command handler failed with an unknown exception");
151 }
152}
153
155 auto failure = [](EditorStatus status, const char* rule, std::string message) {
156 return eve::editing::failed<CommandPlan>(status, RuleId(rule), std::move(message));
157 };
158 auto it = std::find_if(commands_.begin(), commands_.end(),
159 [&](const Registration& entry) { return entry.descriptor.id == request.id; });
160 if (it == commands_.end())
161 return failure(EditorStatus::NotFound, "editor.command.not-found",
162 "Command is not registered: " + request.id.value());
163 if (auto policy = checkExecutionPolicy(it->descriptor, request.id, request.source, request.payload, profile);
164 !policy.ok())
165 return Result<CommandPlan>::failure(std::move(policy).status());
166 if (request.expectedRevision && *request.expectedRevision != request.context.targetRevision)
167 return failure(EditorStatus::Conflict, "editor.command.revision-conflict",
168 "Expected revision does not match the captured context");
169 if (!it->planner)
170 return failure(EditorStatus::Unsupported, "editor.command.planning-unsupported",
171 "Command does not provide a planning handler");
172 try {
173 Result<CommandPlan> result = it->planner(request);
174 if (result.ok()) {
175 result.value().command = request.id;
176 result.value().target = request.context.target;
177 result.value().baseRevision = request.context.targetRevision;
178 if (result.value().targetGeneration == 0)
179 result.value().targetGeneration = request.context.targetGeneration;
180 result.value().commandGeneration = it->generation;
181 result.value().plannedPayload = request.payload;
182 result.value().plannedSource = request.source;
183 if (result.value().id.empty())
184 result.value().id = PlanId(request.id.value() + ".plan." + std::to_string(++planSequence_));
185 }
186 return result;
187 } catch (const std::exception& exception) {
188 return failure(EditorStatus::Failed, "editor.command.planner-exception", exception.what());
189 } catch (...) {
190 return failure(EditorStatus::Failed, "editor.command.planner-exception",
191 "Command planner failed with an unknown exception");
192 }
193}
194
196 const CommandPlan& plan,
197 const HostProfile& profile) const {
198 auto failure = [](EditorStatus status, const char* rule, std::string message) {
199 return eve::editing::failed<TransactionReceipt>(status, RuleId(rule), std::move(message));
200 };
201 if (plan.id.empty() || plan.command != request.id || plan.target != request.context.target)
202 return failure(EditorStatus::Rejected, "editor.command.invalid-plan",
203 "Plan identity or target does not match the request");
204 if (plan.baseRevision != request.context.targetRevision)
205 return failure(EditorStatus::Conflict, "editor.command.stale-plan",
206 "Plan was produced for another target revision");
207 if (plan.targetGeneration != request.context.targetGeneration)
208 return failure(EditorStatus::Conflict, "editor.command.stale-target-generation",
209 "Plan was produced for another target lifetime");
210 if (request.expectedRevision && *request.expectedRevision != request.context.targetRevision)
211 return failure(EditorStatus::Conflict, "editor.command.revision-conflict",
212 "Expected revision does not match the captured context");
213 auto it = std::find_if(commands_.begin(), commands_.end(),
214 [&](const Registration& entry) { return entry.descriptor.id == request.id; });
215 if (it == commands_.end())
216 return failure(EditorStatus::NotFound, "editor.command.not-found",
217 "Command is not registered: " + request.id.value());
218 if (plan.commandGeneration != it->generation)
219 return failure(EditorStatus::Conflict, "editor.command.stale-registration",
220 "Command registration changed after planning");
221 if (auto policy = checkExecutionPolicy(it->descriptor, request.id, request.source, request.payload, profile);
222 !policy.ok())
223 return Result<TransactionReceipt>::failure(std::move(policy).status());
224 if (request.payload != plan.plannedPayload || request.source != plan.plannedSource)
225 return failure(EditorStatus::Rejected, "editor.command.plan-input-mismatch",
226 "Plan payload or source differs from the validated request");
227 if (!it->planExecutor)
228 return failure(EditorStatus::Unsupported, "editor.command.execution-unsupported",
229 "Command does not provide a plan executor");
230 try {
231 return it->planExecutor(request, plan);
232 } catch (const std::exception& exception) {
233 return failure(EditorStatus::Failed, "editor.command.executor-exception", exception.what());
234 } catch (...) {
235 return failure(EditorStatus::Failed, "editor.command.executor-exception",
236 "Command executor failed with an unknown exception");
237 }
238}
239
240} // namespace eve::editor
double value
Value::Object payload
std::string descriptor
std::string message
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
HSQOBJECT handler
std::string error
Definition Package.cpp:60
int removed
const UnitySourceAsset & source
const VegetationPresetContext & context
Move-only operation result carrying either a value or Status.
Definition Result.h:155
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
Deterministic owning value tree shared by authoring hosts.
size_t unregisterOwner(const std::string &ownerModule)
Remove every command registered by a module.
Result< EditorValue > execute(const CommandId &id, const CommandContext &context, const EditorValue &payload) const
Execute a command after payload and profile checks.
bool supportsPlanning(const CommandId &id) const
Return whether the registered command requires plan/commit execution.
const CommandDescriptor * find(const CommandId &id) const
Return command metadata or nullptr when it is not registered.
Result< EditorValue > registerCommand(CommandDescriptor descriptor, EditorCommandHandler handler, bool replace=false)
Register one command.
std::vector< CommandDescriptor > commands(const HostProfile &profile) const
Return descriptors visible to a host profile.
Result< EditorValue > registerPlannedCommand(CommandDescriptor descriptor, EditorCommandPlanner planner, EditorCommandPlanExecutor executor, bool replace=false)
Register a side-effect-free planner and explicit plan executor.
void clear()
Remove all registrations. Primarily useful for isolated tests.
Result< TransactionReceipt > executePlan(const CommandRequest &request, const CommandPlan &plan, const HostProfile &profile) const
Execute a plan only when its lifetime, registration, policy and input bindings remain valid.
Result< CommandPlan > plan(const CommandRequest &request, const HostProfile &profile) const
Produce a side-effect-free plan bound to the target, command generation, payload and source.
bool unregisterCommand(const CommandId &id, const std::string &ownerModule={})
Remove one command, optionally requiring a matching owner.
Upper capability boundary for one editor host.
bool hasFeatures(HostFeature required) const
True when features.
bool allowsCommand(const CommandId &id) const
Allows command.
std::function< Result< TransactionReceipt >(const CommandRequest &, const CommandPlan &)> EditorCommandPlanExecutor
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
eve::editing::PlanId PlanId
Definition EditorIds.h:35
eve::editing::CommandId CommandId
Definition EditorIds.h:30
std::function< Result< EditorValue >(const CommandContext &, const EditorValue &)> EditorCommandHandler
eve::editing::RuleId RuleId
Definition EditorIds.h:32
eve::editing::CommandSource CommandSource
Compatibility alias for the UI-independent authoring command source.
std::function< Result< CommandPlan >(const CommandRequest &)> EditorCommandPlanner
eve::StatusCode EditorStatus
Definition EditorResult.h:8
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
CommandPlan public API.
CommandRequest public API.
Immutable execution context captured for a synchronous command.
Presentation and execution metadata for a registered command.