|
|
/*
* This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2024 Jon Evans <jon@craftyjon.com> * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <magic_enum.hpp>
#include <nlohmann/json.hpp>
#include <wx/log.h>
#include <wx/regex.h>
#include <wx/stdstream.h>
#include <wx/wfstream.h>
#include <api/api_plugin.h>
#include <api/api_plugin_manager.h>
#include <json_conversions.h>
bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson ){ // TODO move to tl::expected and give user feedback about parse errors
try { type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(), magic_enum::case_insensitive ) .value_or( PLUGIN_RUNTIME_TYPE::INVALID );
} catch( ... ) { return false; }
return type != PLUGIN_RUNTIME_TYPE::INVALID;}
struct API_PLUGIN_CONFIG{ API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile );
bool valid; wxString identifier; wxString name; wxString description; PLUGIN_RUNTIME runtime; std::vector<PLUGIN_ACTION> actions;
API_PLUGIN& parent;};
API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile ) : parent( aParent ){ valid = false;
if( !aConfigFile.IsFileReadable() ) return;
wxLogTrace( traceApi, "Plugin: parsing config file" );
wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) ); wxStdInputStream fstream( fp );
nlohmann::json js;
try { js = nlohmann::json::parse( fstream, nullptr, /* allow_exceptions = */ true, /* ignore_comments = */ true ); } catch( ... ) { wxLogTrace( traceApi, "Plugin: exception during parse" ); return; }
// TODO add schema and validate
// All of these are required; any exceptions here leave us with valid == false
try { identifier = js.at( "identifier" ).get<wxString>(); name = js.at( "name" ).get<wxString>(); description = js.at( "description" ).get<wxString>();
if( !runtime.FromJson( js.at( "runtime" ) ) ) { wxLogTrace( traceApi, "Plugin: error parsing runtime section" ); return; } } catch( ... ) { wxLogTrace( traceApi, "Plugin: exception while parsing required keys" ); return; }
// At minimum, we need a reverse-DNS style identifier with two dots and a 2+ character TLD
wxRegEx identifierRegex( wxS( "[\\w\\d]{2,}\\.[\\w\\d]+\\.[\\w\\d]+" ) );
if( !identifierRegex.Matches( identifier ) ) { wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements", identifier ) ); return; }
wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
try { const nlohmann::json& actionsJs = js.at( "actions" );
if( actionsJs.is_array() ) { for( const nlohmann::json& actionJs : actionsJs ) { if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) ) { a->identifier = wxString::Format( "%s.%s", identifier, a->identifier ); wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s", a->identifier ) ); actions.emplace_back( *a ); } } } } catch( ... ) { wxLogTrace( traceApi, "Plugin: exception while parsing actions" ); }
valid = true;}
API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile ) : m_configFile( aConfigFile ), m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile ) ){}
API_PLUGIN::~API_PLUGIN(){}
bool API_PLUGIN::IsOk() const{ return m_config->valid;}
const wxString& API_PLUGIN::Identifier() const{ return m_config->identifier;}
const wxString& API_PLUGIN::Name() const{ return m_config->name;}
const wxString& API_PLUGIN::Description() const{ return m_config->description;}
const PLUGIN_RUNTIME& API_PLUGIN::Runtime() const{ return m_config->runtime;}
const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const{ return m_config->actions;}
wxString API_PLUGIN::BasePath() const{ return m_configFile.GetPath();}
std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson ){ // TODO move to tl::expected and give user feedback about parse errors
PLUGIN_ACTION action( *this );
try { action.identifier = aJson.at( "identifier" ).get<wxString>(); wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) ); action.name = aJson.at( "name" ).get<wxString>(); action.description = aJson.at( "description" ).get<wxString>(); action.entrypoint = aJson.at( "entrypoint" ).get<wxString>(); action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>(); } catch( ... ) { wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" ); return std::nullopt; }
wxFileName f( action.entrypoint );
if( !f.IsRelative() ) { wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping", action.entrypoint ) ); return std::nullopt; }
f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
if( !f.IsFileReadable() ) { wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable", f.GetFullPath() ) ); }
if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() ) { for( const nlohmann::json& argJs : aJson.at( "args" ) ) { try { action.args.emplace_back( argJs.get<wxString>() ); } catch( ... ) { wxLogTrace( traceApi, "Plugin: exception while parsing action args" ); continue; } } }
if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() ) { for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) ) { try { action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>( scopeJs.get<std::string>(), magic_enum::case_insensitive ) .value_or( PLUGIN_ACTION_SCOPE::INVALID ) ); } catch( ... ) { wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" ); continue; } } }
auto handleBitmap = [&]( const std::string& aKey, wxBitmapBundle& aDest ) { if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() ) { wxVector<wxBitmap> bitmaps;
for( const nlohmann::json& iconJs : aJson.at( aKey ) ) { wxFileName iconFile;
try { iconFile = iconJs.get<wxString>(); } catch( ... ) { continue; }
iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
wxLogTrace( traceApi, wxString::Format( "Plugin: action %s: loading icon %s", action.identifier, iconFile.GetFullPath() ) );
if( !iconFile.IsFileReadable() ) { wxLogTrace( traceApi, "Plugin: icon file could not be read" ); continue; }
wxBitmap bmp; // TODO: If necessary; support types other than PNG
bmp.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
if( bmp.IsOk() ) bitmaps.push_back( bmp ); else wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" ); }
aDest = wxBitmapBundle::FromBitmaps( bitmaps ); } };
handleBitmap( "icons-light", action.icon_light ); handleBitmap( "icons-dark", action.icon_dark );
return action;}
|