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