KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_plugin.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
21#include <magic_enum.hpp>
22#include <json_common.h>
23#include <wx/log.h>
24#include <wx/regex.h>
25#include <wx/stdstream.h>
26#include <wx/wfstream.h>
27
28#include <api/api_plugin.h>
30#include <api/api_utils.h>
31#include <json_conversions.h>
33
34
39
40
41void LOGGING_ERROR_HANDLER::error( const nlohmann::json::json_pointer& ptr,
42 const nlohmann::json& instance,
43 const std::string& message )
44{
45 m_hasError = true;
46 wxLogTrace( traceApi,
47 wxString::Format( wxS( "JSON error: at %s, value:\n%s\n%s" ),
48 ptr.to_string(), instance.dump(), message ) );
49}
50
51
52bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
53{
54 // TODO move to tl::expected and give user feedback about parse errors
55
56 try
57 {
58 type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(),
59 magic_enum::case_insensitive )
61
62
63 }
64 catch( ... )
65 {
66 return false;
67 }
68
70}
71
72
74{
75 API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
76 const JSON_SCHEMA_VALIDATOR& aValidator );
77
78 bool valid;
79 wxString identifier;
80 wxString name;
81 wxString description;
83 std::vector<PLUGIN_ACTION> actions;
84
86};
87
88
89API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
90 const JSON_SCHEMA_VALIDATOR& aValidator ) :
91 parent( aParent )
92{
93 valid = false;
94
95 if( !aConfigFile.IsFileReadable() )
96 return;
97
98 wxLogTrace( traceApi, "Plugin: parsing config file" );
99
100 wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) );
101 wxStdInputStream fstream( fp );
102
103 nlohmann::json js;
104
105 try
106 {
107 js = nlohmann::json::parse( fstream, nullptr,
108 /* allow_exceptions = */ true,
109 /* ignore_comments = */ true );
110 }
111 catch( ... )
112 {
113 wxLogTrace( traceApi, "Plugin: exception during parse" );
114 return;
115 }
116
117 LOGGING_ERROR_HANDLER handler;
118 aValidator.Validate( js, handler, nlohmann::json_uri( "#/definitions/Plugin" ) );
119
120 if( !handler.HasError() )
121 wxLogTrace( traceApi, "Plugin: schema validation successful" );
122
123 // All of these are required; any exceptions here leave us with valid == false
124 try
125 {
126 identifier = js.at( "identifier" ).get<wxString>();
127 name = js.at( "name" ).get<wxString>();
128 description = js.at( "description" ).get<wxString>();
129
130 if( !runtime.FromJson( js.at( "runtime" ) ) )
131 {
132 wxLogTrace( traceApi, "Plugin: error parsing runtime section" );
133 return;
134 }
135 }
136 catch( ... )
137 {
138 wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
139 return;
140 }
141
143 {
144 wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
145 identifier ) );
146 return;
147 }
148
149 wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
150
151 try
152 {
153 const nlohmann::json& actionsJs = js.at( "actions" );
154
155 if( actionsJs.is_array() )
156 {
157 for( const nlohmann::json& actionJs : actionsJs )
158 {
159 if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
160 {
161 a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
162 wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
163 a->identifier ) );
164 actions.emplace_back( *a );
165 }
166 }
167 }
168 }
169 catch( ... )
170 {
171 wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
172 return;
173 }
174
175 valid = true;
176}
177
178
179API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator ) :
180 m_configFile( aConfigFile ),
181 m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile, aValidator ) )
182{
183}
184
185
189
190
192{
193 return m_config->valid;
194}
195
196
197bool API_PLUGIN::IsValidIdentifier( const wxString& aIdentifier )
198{
199 // At minimum, we need a reverse-DNS style identifier with two dots and a 2+ character TLD
200 wxRegEx identifierRegex( wxS( "[\\w\\d]{2,}\\.[\\w\\d]+\\.[\\w\\d]+" ) );
201 return identifierRegex.Matches( aIdentifier );
202}
203
204
205const wxString& API_PLUGIN::Identifier() const
206{
207 return m_config->identifier;
208}
209
210
211const wxString& API_PLUGIN::Name() const
212{
213 return m_config->name;
214}
215
216
217const wxString& API_PLUGIN::Description() const
218{
219 return m_config->description;
220}
221
222
224{
225 return m_config->runtime;
226}
227
228
229const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
230{
231 return m_config->actions;
232}
233
234
235wxString API_PLUGIN::BasePath() const
236{
237 return m_configFile.GetPath();
238}
239
240
241wxString API_PLUGIN::ActionSettingsKey( const PLUGIN_ACTION& aAction ) const
242{
243 return Identifier() + "." + aAction.identifier;
244}
245
246
247
248std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
249{
250 // TODO move to tl::expected and give user feedback about parse errors
251 PLUGIN_ACTION action( *this );
252
253 try
254 {
255 action.identifier = aJson.at( "identifier" ).get<wxString>();
256 wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
257 action.name = aJson.at( "name" ).get<wxString>();
258 action.description = aJson.at( "description" ).get<wxString>();
259 action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
260 action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
261 }
262 catch( ... )
263 {
264 wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
265 return std::nullopt;
266 }
267
268 wxFileName f( action.entrypoint );
269
270 if( !f.IsRelative() )
271 {
272 wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
273 action.entrypoint ) );
274 return std::nullopt;
275 }
276
277 f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
278
279 if( !f.IsFileReadable() )
280 {
281 wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
282 f.GetFullPath() ) );
283 }
284
285 if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
286 {
287 for( const nlohmann::json& argJs : aJson.at( "args" ) )
288 {
289 try
290 {
291 action.args.emplace_back( argJs.get<wxString>() );
292 }
293 catch( ... )
294 {
295 wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
296 continue;
297 }
298 }
299 }
300
301 if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
302 {
303 for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
304 {
305 try
306 {
307 action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
308 scopeJs.get<std::string>(), magic_enum::case_insensitive )
309 .value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
310 }
311 catch( ... )
312 {
313 wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
314 continue;
315 }
316 }
317 }
318
319 auto handleBitmap =
320 [&]( const std::string& aKey, wxBitmapBundle& aDest )
321 {
322 if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
323 {
324 wxVector<wxBitmap> bitmaps;
325
326 for( const nlohmann::json& iconJs : aJson.at( aKey ) )
327 {
328 wxFileName iconFile;
329
330 try
331 {
332 iconFile = iconJs.get<wxString>();
333 }
334 catch( ... )
335 {
336 continue;
337 }
338
339 iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
340
341 wxLogTrace( traceApi,
342 wxString::Format( "Plugin: action %s: loading icon %s",
343 action.identifier, iconFile.GetFullPath() ) );
344
345
346 if( !iconFile.IsFileReadable() )
347 {
348 wxLogTrace( traceApi, "Plugin: icon file could not be read" );
349 continue;
350 }
351
352 wxBitmap bmp;
353 // TODO: If necessary; support types other than PNG
354 bmp.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
355
356 if( bmp.IsOk() )
357 bitmaps.push_back( bmp );
358 else
359 wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
360 }
361
362 aDest = wxBitmapBundle::FromBitmaps( bitmaps );
363 }
364 };
365
366 handleBitmap( "icons-light", action.icon_light );
367 handleBitmap( "icons-dark", action.icon_dark );
368
369 return action;
370}
A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad via the I...
Definition api_plugin.h:95
const PLUGIN_RUNTIME & Runtime() const
const wxString & Name() const
const std::vector< PLUGIN_ACTION > & Actions() const
friend struct API_PLUGIN_CONFIG
Definition api_plugin.h:116
wxString ActionSettingsKey(const PLUGIN_ACTION &aAction) const
const wxString & Identifier() const
std::unique_ptr< API_PLUGIN_CONFIG > m_config
Definition api_plugin.h:122
wxFileName m_configFile
Definition api_plugin.h:120
bool IsOk() const
wxString BasePath() const
static bool IsValidIdentifier(const wxString &aIdentifier)
const wxString & Description() const
API_PLUGIN(const wxFileName &aConfigFile, const JSON_SCHEMA_VALIDATOR &aValidator)
std::optional< PLUGIN_ACTION > createActionFromJson(const nlohmann::json &aJson)
nlohmann::json Validate(const nlohmann::json &aJson, nlohmann::json_schema::error_handler &aErrorHandler, const nlohmann::json_uri &aInitialUri=nlohmann::json_uri("#")) const
bool HasError() const
Definition api_plugin.h:143
void error(const nlohmann::json::json_pointer &ptr, const nlohmann::json &instance, const std::string &message) override
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:27
STL namespace.
API_PLUGIN_CONFIG(API_PLUGIN &aParent, const wxFileName &aConfigFile, const JSON_SCHEMA_VALIDATOR &aValidator)
PLUGIN_RUNTIME runtime
API_PLUGIN & parent
std::vector< PLUGIN_ACTION > actions
An action performed by a plugin via the IPC API.
Definition api_plugin.h:71
wxBitmapBundle icon_light
Definition api_plugin.h:83
wxString name
Definition api_plugin.h:77
wxString description
Definition api_plugin.h:78
std::set< PLUGIN_ACTION_SCOPE > scopes
Definition api_plugin.h:81
wxString identifier
Definition api_plugin.h:76
wxString entrypoint
Definition api_plugin.h:80
wxBitmapBundle icon_dark
Definition api_plugin.h:84
std::vector< wxString > args
Definition api_plugin.h:82
bool FromJson(const nlohmann::json &aJson)
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:61