KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_handler_editor.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 Jon Evans <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
22#include <api/api_utils.h>
23#include <eda_base_frame.h>
24#include <eda_item.h>
25#include <wx/wx.h>
26
27using namespace kiapi::common::commands;
28
29
41
42
45{
46 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
47 return tl::unexpected( *busy );
48
49 if( m_commits.count( aCtx.ClientName ) )
50 {
51 ApiResponseStatus e;
52 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
53 e.set_error_message( fmt::format( "the client {} already has a commit in progress",
54 aCtx.ClientName ) );
55 return tl::unexpected( e );
56 }
57
58 wxASSERT( !m_activeClients.count( aCtx.ClientName ) );
59
60 BeginCommitResponse response;
61
62 KIID id;
63 m_commits[aCtx.ClientName] = std::make_pair( id, createCommit() );
64 response.mutable_id()->set_value( id.AsStdString() );
65
66 m_activeClients.insert( aCtx.ClientName );
67
68 return response;
69}
70
71
73 const HANDLER_CONTEXT<EndCommit>& aCtx )
74{
75 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
76 return tl::unexpected( *busy );
77
78 if( !m_commits.count( aCtx.ClientName ) )
79 {
80 ApiResponseStatus e;
81 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
82 e.set_error_message( fmt::format( "the client {} does not has a commit in progress",
83 aCtx.ClientName ) );
84 return tl::unexpected( e );
85 }
86
87 wxASSERT( m_activeClients.count( aCtx.ClientName ) );
88
89 const std::pair<KIID, std::unique_ptr<COMMIT>>& pair = m_commits.at( aCtx.ClientName );
90 const KIID& id = pair.first;
91 const std::unique_ptr<COMMIT>& commit = pair.second;
92
93 EndCommitResponse response;
94
95 // Do not check IDs with drop; it is a safety net in case the id was lost on the client side
96 switch( aCtx.Request.action() )
97 {
98 case kiapi::common::commands::CMA_DROP:
99 {
100 commit->Revert();
101 m_commits.erase( aCtx.ClientName );
102 m_activeClients.erase( aCtx.ClientName );
103 break;
104 }
105
106 case kiapi::common::commands::CMA_COMMIT:
107 {
108 if( aCtx.Request.id().value().compare( id.AsStdString() ) != 0 )
109 {
110 ApiResponseStatus e;
111 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
112 e.set_error_message( fmt::format( "the id {} does not match the commit in progress",
113 aCtx.Request.id().value() ) );
114 return tl::unexpected( e );
115 }
116
117 pushCurrentCommit( aCtx.ClientName, wxString( aCtx.Request.message().c_str(), wxConvUTF8 ) );
118 break;
119 }
120
121 default:
122 break;
123 }
124
125 return response;
126}
127
128
129COMMIT* API_HANDLER_EDITOR::getCurrentCommit( const std::string& aClientName )
130{
131 if( !m_commits.count( aClientName ) )
132 {
133 KIID id;
134 m_commits[aClientName] = std::make_pair( id, createCommit() );
135 }
136
137 return m_commits.at( aClientName ).second.get();
138}
139
140
141void API_HANDLER_EDITOR::pushCurrentCommit( const std::string& aClientName,
142 const wxString& aMessage )
143{
144 auto it = m_commits.find( aClientName );
145
146 if( it == m_commits.end() )
147 return;
148
149 it->second.second->Push( aMessage.IsEmpty() ? m_defaultCommitMessage : aMessage );
150 m_commits.erase( it );
151 m_activeClients.erase( aClientName );
152}
153
154
156{
157 if( !validateDocumentInternal( aDocument ) )
158 {
159 ApiResponseStatus e;
160 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
161 e.set_error_message( fmt::format( "the requested document {} is not open",
162 aDocument.board_filename() ) );
163 return tl::unexpected( e );
164 }
165
166 return true;
167}
168
169
171 const types::ItemHeader& aHeader )
172{
173 if( !aHeader.has_document() || aHeader.document().type() != thisDocumentType() )
174 {
175 ApiResponseStatus e;
176 e.set_status( ApiStatusCode::AS_UNHANDLED );
177 // No error message, this is a flag that the server should try a different handler
178 return tl::unexpected( e );
179 }
180
181 HANDLER_RESULT<bool> documentValidation = validateDocument( aHeader.document() );
182
183 if( !documentValidation )
184 return tl::unexpected( documentValidation.error() );
185
186 if( !validateDocumentInternal( aHeader.document() ) )
187 {
188 ApiResponseStatus e;
189 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
190 e.set_error_message( fmt::format( "the requested document {} is not open",
191 aHeader.document().board_filename() ) );
192 return tl::unexpected( e );
193 }
194
195 if( aHeader.has_container() )
196 {
197 return KIID( aHeader.container().value() );
198 }
199
200 // Valid header, but no container provided
201 return std::nullopt;
202}
203
204
205std::optional<ApiResponseStatus> API_HANDLER_EDITOR::checkForBusy()
206{
207 if( !m_frame )
208 return std::nullopt;
209
210 if( !m_frame->CanAcceptApiCommands() )
211 {
212 ApiResponseStatus e;
213 e.set_status( ApiStatusCode::AS_BUSY );
214 e.set_error_message( "KiCad is busy and cannot respond to API requests right now" );
215 return e;
216 }
217
218 return std::nullopt;
219}
220
221
223 const HANDLER_CONTEXT<CreateItems>& aCtx )
224{
225 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
226 return tl::unexpected( *busy );
227
228 CreateItemsResponse response;
229
231 aCtx.ClientName,
232 aCtx.Request.header(), aCtx.Request.items(),
233 [&]( const ItemStatus& aStatus, const google::protobuf::Any& aItem )
234 {
235 ItemCreationResult itemResult;
236 itemResult.mutable_status()->CopyFrom( aStatus );
237 itemResult.mutable_item()->CopyFrom( aItem );
238 response.mutable_created_items()->Add( std::move( itemResult ) );
239 } );
240
241 if( !result.has_value() )
242 return tl::unexpected( result.error() );
243
244 response.set_status( *result );
245 return response;
246}
247
248
250 const HANDLER_CONTEXT<UpdateItems>& aCtx )
251{
252 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
253 return tl::unexpected( *busy );
254
255 UpdateItemsResponse response;
256
258 aCtx.ClientName,
259 aCtx.Request.header(), aCtx.Request.items(),
260 [&]( const ItemStatus& aStatus, const google::protobuf::Any& aItem )
261 {
262 ItemUpdateResult itemResult;
263 itemResult.mutable_status()->CopyFrom( aStatus );
264 itemResult.mutable_item()->CopyFrom( aItem );
265 response.mutable_updated_items()->Add( std::move( itemResult ) );
266 } );
267
268 if( !result.has_value() )
269 return tl::unexpected( result.error() );
270
271 response.set_status( *result );
272 return response;
273}
274
275
277 const HANDLER_CONTEXT<DeleteItems>& aCtx )
278{
279 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
280 return tl::unexpected( *busy );
281
282 if( !validateItemHeaderDocument( aCtx.Request.header() ) )
283 {
284 ApiResponseStatus e;
285 // No message needed for AS_UNHANDLED; this is an internal flag for the API server
286 e.set_status( ApiStatusCode::AS_UNHANDLED );
287 return tl::unexpected( e );
288 }
289
290 std::map<KIID, ItemDeletionStatus> itemsToDelete;
291
292 for( const kiapi::common::types::KIID& kiidBuf : aCtx.Request.item_ids() )
293 {
294 if( !kiidBuf.value().empty() )
295 {
296 KIID kiid( kiidBuf.value() );
297 itemsToDelete[kiid] = ItemDeletionStatus::IDS_NONEXISTENT;
298 }
299 }
300
301 if( itemsToDelete.empty() )
302 {
303 ApiResponseStatus e;
304 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
305 e.set_error_message( "no valid items to delete were given" );
306 return tl::unexpected( e );
307 }
308
309 deleteItemsInternal( itemsToDelete, aCtx.ClientName );
310
311 DeleteItemsResponse response;
312
313 for( const auto& [id, status] : itemsToDelete )
314 {
315 ItemDeletionResult result;
316 result.mutable_id()->set_value( id.AsStdString() );
317 result.set_status( status );
318 }
319
320 response.set_status( kiapi::common::types::ItemRequestStatus::IRS_OK );
321 return response;
322}
323
324
326 const HANDLER_CONTEXT<HitTest>& aCtx )
327{
328 if( std::optional<ApiResponseStatus> busy = checkForBusy() )
329 return tl::unexpected( *busy );
330
331 if( !validateItemHeaderDocument( aCtx.Request.header() ) )
332 {
333 ApiResponseStatus e;
334 // No message needed for AS_UNHANDLED; this is an internal flag for the API server
335 e.set_status( ApiStatusCode::AS_UNHANDLED );
336 return tl::unexpected( e );
337 }
338
339 HitTestResponse response;
340
341 std::optional<EDA_ITEM*> item = getItemFromDocument( aCtx.Request.header().document(),
342 KIID( aCtx.Request.id().value() ) );
343
344 if( !item )
345 {
346 ApiResponseStatus e;
347 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
348 e.set_error_message( "the requested item ID is not present in the given document" );
349 return tl::unexpected( e );
350 }
351
352 if( ( *item )->HitTest( UnpackVector2( aCtx.Request.position() ), aCtx.Request.tolerance() ) )
353 response.set_result( HitTestResult::HTR_HIT );
354 else
355 response.set_result( HitTestResult::HTR_NO_HIT );
356
357 return response;
358}
tl::expected< T, ApiResponseStatus > HANDLER_RESULT
Definition api_handler.h:45
virtual std::unique_ptr< COMMIT > createCommit()=0
Override this to create an appropriate COMMIT subclass for the frame in question.
HANDLER_RESULT< bool > validateDocument(const DocumentSpecifier &aDocument)
HANDLER_RESULT< commands::DeleteItemsResponse > handleDeleteItems(const HANDLER_CONTEXT< commands::DeleteItems > &aCtx)
virtual std::optional< EDA_ITEM * > getItemFromDocument(const DocumentSpecifier &aDocument, const KIID &aId)=0
HANDLER_RESULT< std::optional< KIID > > validateItemHeaderDocument(const kiapi::common::types::ItemHeader &aHeader)
If the header is valid, returns the item container.
HANDLER_RESULT< commands::EndCommitResponse > handleEndCommit(const HANDLER_CONTEXT< commands::EndCommit > &aCtx)
API_HANDLER_EDITOR(EDA_BASE_FRAME *aFrame=nullptr)
HANDLER_RESULT< commands::CreateItemsResponse > handleCreateItems(const HANDLER_CONTEXT< commands::CreateItems > &aCtx)
COMMIT * getCurrentCommit(const std::string &aClientName)
virtual void pushCurrentCommit(const std::string &aClientName, const wxString &aMessage)
virtual HANDLER_RESULT< ItemRequestStatus > handleCreateUpdateItemsInternal(bool aCreate, const std::string &aClientName, const types::ItemHeader &aHeader, const google::protobuf::RepeatedPtrField< google::protobuf::Any > &aItems, std::function< void(commands::ItemStatus, google::protobuf::Any)> aItemHandler)=0
std::set< std::string > m_activeClients
std::map< std::string, std::pair< KIID, std::unique_ptr< COMMIT > > > m_commits
HANDLER_RESULT< commands::UpdateItemsResponse > handleUpdateItems(const HANDLER_CONTEXT< commands::UpdateItems > &aCtx)
virtual void deleteItemsInternal(std::map< KIID, ItemDeletionStatus > &aItemsToDelete, const std::string &aClientName)=0
virtual std::optional< ApiResponseStatus > checkForBusy()
Checks if the editor can accept commands.
virtual types::DocumentType thisDocumentType() const =0
Override this to specify which document type this editor handles.
HANDLER_RESULT< commands::BeginCommitResponse > handleBeginCommit(const HANDLER_CONTEXT< commands::BeginCommit > &aCtx)
HANDLER_RESULT< commands::HitTestResponse > handleHitTest(const HANDLER_CONTEXT< commands::HitTest > &aCtx)
EDA_BASE_FRAME * m_frame
virtual bool validateDocumentInternal(const DocumentSpecifier &aDocument) const =0
static const wxString m_defaultCommitMessage
void registerHandler(HANDLER_RESULT< ResponseType >(HandlerType::*aHandler)(const HANDLER_CONTEXT< RequestType > &))
Registers an API command handler for the given message types.
Definition api_handler.h:93
Represent a set of changes (additions, deletions or modifications) of a data model (e....
Definition commit.h:72
The base frame for deriving all KiCad main window classes.
Definition kiid.h:48
Base window classes and related definitions.
KICOMMON_API VECTOR2I UnpackVector2(const types::Vector2 &aInput)
Definition api_utils.cpp:86
std::string ClientName
Definition api_handler.h:51
RequestMessageType Request
Definition api_handler.h:52
wxString result
Test unit parsing edge cases and error handling.