/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2024 Jon Evans * 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 . */ #include #include #include #include #include #include #include #include #include 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( aJson.at( "type" ).get(), 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 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(); name = js.at( "name" ).get(); description = js.at( "description" ).get(); 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 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( *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& API_PLUGIN::Actions() const { return m_config->actions; } wxString API_PLUGIN::BasePath() const { return m_configFile.GetPath(); } std::optional 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(); wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) ); action.name = aJson.at( "name" ).get(); action.description = aJson.at( "description" ).get(); action.entrypoint = aJson.at( "entrypoint" ).get(); action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get(); } 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() ); } 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( scopeJs.get(), 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 bitmaps; for( const nlohmann::json& iconJs : aJson.at( aKey ) ) { wxFileName iconFile; try { iconFile = iconJs.get(); } 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; }