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