From 2bcad18cc22fe204f32ede6fe20624a318fa6fed Mon Sep 17 00:00:00 2001 From: Andre Iwers Date: Tue, 19 Sep 2023 01:09:21 +0000 Subject: [PATCH] ADDED: HTTP library support Fixes https://gitlab.com/kicad/code/kicad/-/issues/12027 --- common/CMakeLists.txt | 4 + common/http_lib/http_lib_connection.cpp | 463 ++++++++++++++++++ common/http_lib/http_lib_settings.cpp | 51 ++ common/wildcards_and_files_ext.cpp | 1 + eeschema/CMakeLists.txt | 1 + eeschema/sch_io_mgr.cpp | 5 + eeschema/sch_io_mgr.h | 1 + .../http_lib/sch_http_lib_plugin.cpp | 450 +++++++++++++++++ .../http_lib/sch_http_lib_plugin.h | 130 +++++ include/http_lib/http_lib_connection.h | 121 +++++ include/http_lib/http_lib_settings.h | 86 ++++ include/wildcards_and_files_ext.h | 1 + 12 files changed, 1314 insertions(+) create mode 100644 common/http_lib/http_lib_connection.cpp create mode 100644 common/http_lib/http_lib_settings.cpp create mode 100644 eeschema/sch_plugins/http_lib/sch_http_lib_plugin.cpp create mode 100644 eeschema/sch_plugins/http_lib/sch_http_lib_plugin.h create mode 100644 include/http_lib/http_lib_connection.h create mode 100644 include/http_lib/http_lib_settings.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 54a5c3acee..f8d6acf1a5 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -561,6 +561,10 @@ set( COMMON_SRCS database/database_connection.cpp database/database_lib_settings.cpp + + http_lib/http_lib_connection.cpp + http_lib/http_lib_settings.cpp + ) add_library( common STATIC diff --git a/common/http_lib/http_lib_connection.cpp b/common/http_lib/http_lib_connection.cpp new file mode 100644 index 0000000000..e038761f10 --- /dev/null +++ b/common/http_lib/http_lib_connection.cpp @@ -0,0 +1,463 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 Andre F. K. Iwers + * + * 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 + +const char* const traceHTTPLib = "KICAD_HTTP_LIB"; + + +HTTP_LIB_CONNECTION::HTTP_LIB_CONNECTION( const HTTP_LIB_SOURCE& aSource, bool aTestConnectionNow ) +{ + m_rootURL = aSource.root_url; + m_token = aSource.token; + m_sourceType = aSource.type; + + if( aTestConnectionNow ) + { + ValidateHTTPLibraryEndpoints(); + } +} + + +HTTP_LIB_CONNECTION::~HTTP_LIB_CONNECTION() +{ + // Do nothing +} + + +bool HTTP_LIB_CONNECTION::ValidateHTTPLibraryEndpoints() +{ + std::string res = ""; + + std::unique_ptr m_curl = createCurlEasyObject(); + m_curl->SetURL( m_rootURL ); + + try + { + m_curl->Perform(); + + res = m_curl->GetBuffer(); + + if( !checkServerResponse( m_curl ) ) + { + return false; + } + + if( res.length() == 0 ) + { + m_lastError += wxString::Format( _( "KiCad received an empty response!" ) + "\n" ); + } + else + { + nlohmann::json response = nlohmann::json::parse( res ); + + // Check that the endpoints exist, if not fail. + if( !response.at( http_endpoint_categories ).empty() + && !response.at( http_endpoint_parts ).empty() ) + m_enpointValid = true; + else + m_enpointValid = false; + } + } + catch( const std::exception& e ) + { + m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", + e.what(), res ); + + wxLogTrace( traceHTTPLib, + wxT( "ValidateHTTPLibraryEndpoints: Exception occurred while testing the API " + "connection: %s" ), + m_lastError ); + + m_enpointValid = false; + } + + if( IsValidEnpoint() ) + { + syncCategories(); + } + + return IsValidEnpoint(); +} + + +bool HTTP_LIB_CONNECTION::IsValidEnpoint() const +{ + return m_enpointValid; +} + + +bool HTTP_LIB_CONNECTION::syncCategories() +{ + if( !IsValidEnpoint() ) + { + wxLogTrace( traceHTTPLib, wxT( "syncCategories: without valid connection!" ) ); + return false; + } + + std::string res = ""; + + std::unique_ptr m_curl = createCurlEasyObject(); + m_curl->SetURL( m_rootURL + http_endpoint_categories + ".json" ); + + try + { + m_curl->Perform(); + + res = m_curl->GetBuffer(); + + if( !checkServerResponse( m_curl ) ) + { + return false; + } + + nlohmann::json response = nlohmann::json::parse( res ); + + // collect the categories in vector + for( const auto& item : response.items() ) + { + HTTP_LIB_CATEGORY category; + + category.id = item.value()["id"].get(); + category.name = item.value()["name"].get(); + + m_categories.push_back( category ); + } + } + catch( const std::exception& e ) + { + m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", + e.what(), res ); + + wxLogTrace( traceHTTPLib, + wxT( "syncCategories: Exception occurred while syncing categories: %s" ), + m_lastError ); + + m_categories.clear(); + + return false; + } + + return true; +} + + +bool HTTP_LIB_CONNECTION::SelectOne( const std::string aPartID, HTTP_LIB_PART& aFetchedPart ) +{ + if( !IsValidEnpoint() ) + { + wxLogTrace( traceHTTPLib, wxT( "SelectOne: without valid connection!" ) ); + return false; + } + + // if the same part is selected again, use cached part + // instead to minimise http requests. + if( m_cached_part.id == aPartID ) + { + aFetchedPart = m_cached_part; + return true; + } + + std::string res = ""; + + std::unique_ptr m_curl = createCurlEasyObject(); + m_curl->SetURL( m_rootURL + fmt::format( http_endpoint_parts + "/{}.json", aPartID ) ); + + try + { + m_curl->Perform(); + + res = m_curl->GetBuffer(); + + if( !checkServerResponse( m_curl ) ) + { + return false; + } + + nlohmann::json response = nlohmann::json::parse( res ); + std::string key = ""; + std::string value = ""; + + // the id used to identify the part, the name is needed to show a human-readable + // part descirption to the user inside the symbol chooser dialog + aFetchedPart.id = response.at( "id" ); + + // API might not want to return an optional name. + if( response.contains( "name" ) ) + { + aFetchedPart.name = response.at( "name" ); + } + else + { + aFetchedPart.name = aFetchedPart.id; + } + + aFetchedPart.symbolIdStr = response.at( "symbolIdStr" ); + + // Extract available fields + for( const auto& field : response.at( "fields" ).items() ) + { + bool visible = true; + + // name of the field + key = field.key(); + + // this is a dict + auto& properties = field.value(); + + value = properties.at( "value" ); + + // check if user wants to display field in schematic + if( properties.contains( "visible" ) ) + { + std::string buf = properties.at( "visible" ); + visible = boolFromString( buf ); + } + + // Add field to fields list + if( key.length() ) + { + aFetchedPart.fields[key] = std::make_tuple( value, visible ); + } + } + } + catch( const std::exception& e ) + { + m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", + e.what(), res ); + + wxLogTrace( traceHTTPLib, + wxT( "SelectOne: Exception occurred while retrieving part from REST API: %s" ), + m_lastError ); + + return false; + } + + m_cached_part = aFetchedPart; + + return true; +} + + +bool HTTP_LIB_CONNECTION::SelectAll( const HTTP_LIB_CATEGORY& aCategory, + std::vector& aParts ) +{ + if( !IsValidEnpoint() ) + { + wxLogTrace( traceHTTPLib, wxT( "SelectAll: without valid connection!" ) ); + return false; + } + + std::string res = ""; + + std::unique_ptr m_curl = createCurlEasyObject(); + m_curl->SetURL( m_rootURL + + fmt::format( http_endpoint_parts + "/category/{}.json", aCategory.id ) ); + + try + { + m_curl->Perform(); + + res = m_curl->GetBuffer(); + + nlohmann::json response = nlohmann::json::parse( res ); + std::string key = ""; + std::string value = ""; + + for( nlohmann::json item : response ) + { + //PART result; + HTTP_LIB_PART part; + + part.id = item.at( "id" ); + + // API might not want to return an optional name. + if( item.contains( "name" ) ) + { + part.name = item.at( "name" ); + } + else + { + part.name = part.id; + } + + // add to cache + m_cache[part.name] = std::make_tuple( part.id, aCategory.id ); + + aParts.emplace_back( std::move( part ) ); + } + } + catch( const std::exception& e ) + { + m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", + e.what(), res ); + + wxLogTrace( traceHTTPLib, wxT( "Exception occurred while syncing parts from REST API: %s" ), + m_lastError ); + + return false; + } + + return true; +} + + +bool HTTP_LIB_CONNECTION::checkServerResponse( std::unique_ptr& m_curl ) +{ + + long http_code = 0; + + curl_easy_getinfo( m_curl->GetCurl(), CURLINFO_RESPONSE_CODE, &http_code ); + + if( http_code != 200 ) + { + m_lastError += wxString::Format( _( "API responded with error code: %s" ) + "\n", + httpErrorCodeDescription( http_code ) ); + return false; + } + + return true; +} + + +bool HTTP_LIB_CONNECTION::boolFromString( const std::any& aVal ) +{ + try + { + wxString strval( std::any_cast( aVal ).c_str(), wxConvUTF8 ); + + if( strval.IsEmpty() ) + return true; + + strval.MakeLower(); + + for( const auto& trueVal : { wxS( "true" ), wxS( "yes" ), wxS( "y" ), wxS( "1" ) } ) + { + if( strval.Matches( trueVal ) ) + return true; + } + + for( const auto& falseVal : { wxS( "false" ), wxS( "no" ), wxS( "n" ), wxS( "0" ) } ) + { + if( strval.Matches( falseVal ) ) + return false; + } + } + catch( const std::bad_any_cast& ) + { + } + + return true; +} + +/* +* HTTP response status codes indicate whether a specific HTTP request has been successfully completed. +* Responses are grouped in five classes: +* Informational responses (100 ? 199) +* Successful responses (200 ? 299) +* Redirection messages (300 ? 399) +* Client error responses (400 ? 499) +* Server error responses (500 ? 599) +* +* see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status +*/ +wxString HTTP_LIB_CONNECTION::httpErrorCodeDescription( uint16_t http_code ) +{ + switch( http_code ) + { + case 100: return "100" + _( "Continue" ); + case 101: return "101" + _( "Switching Protocols" ); + case 102: return "102" + _( "Processing" ); + case 103: return "103" + _( "Early Hints" ); + + case 200: return "200" + _( "OK" ); + case 201: return "201" + _( "Created" ); + case 203: return "203" + _( "Non-Authoritative Information" ); + case 204: return "204" + _( "No Content" ); + case 205: return "205" + _( "Reset Content" ); + case 206: return "206" + _( "Partial Content" ); + case 207: return "207" + _( "Multi-Status" ); + case 208: return "208" + _( "Already Reporte" ); + case 226: return "226" + _( "IM Used" ); + + case 300: return "300" + _( "Multiple Choices" ); + case 301: return "301" + _( "Moved Permanently" ); + case 302: return "302" + _( "Found" ); + case 303: return "303" + _( "See Other" ); + case 304: return "304" + _( "Not Modified" ); + case 305: return "305" + _( "Use Proxy (Deprecated)" ); + case 306: return "306" + _( "Unused" ); + case 307: return "307" + _( "Temporary Redirect" ); + case 308: return "308" + _( "Permanent Redirect" ); + + case 400: return "400" + _( "Bad Request" ); + case 401: return "401" + _( "Unauthorized" ); + case 402: return "402" + _( "Payment Required (Experimental)" ); + case 403: return "403" + _( "Forbidden" ); + case 404: return "404" + _( "Not Found" ); + case 405: return "405" + _( "Method Not Allowed" ); + case 406: return "406" + _( "Not Acceptable" ); + case 407: return "407" + _( "Proxy Authentication Required" ); + case 408: return "408" + _( "Request Timeout" ); + case 409: return "409" + _( "Conflict" ); + case 410: return "410" + _( "Gone" ); + case 411: return "411" + _( "Length Required" ); + case 412: return "413" + _( "Payload Too Large" ); + case 414: return "414" + _( "URI Too Long" ); + case 415: return "415" + _( "Unsupported Media Type" ); + case 416: return "416" + _( "Range Not Satisfiable" ); + case 417: return "417" + _( "Expectation Failed" ); + case 418: return "418" + _( "I'm a teapot" ); + case 421: return "421" + _( "Misdirected Request" ); + case 422: return "422" + _( "Unprocessable Conten" ); + case 423: return "423" + _( "Locked" ); + case 424: return "424" + _( "Failed Dependency" ); + case 425: return "425" + _( "Too Early (Experimental)" ); + case 426: return "426" + _( "Upgrade Required" ); + case 428: return "428" + _( "Precondition Required" ); + case 429: return "429" + _( "Too Many Requests" ); + case 431: return "431" + _( "Request Header Fields Too Large" ); + case 451: return "451" + _( "Unavailable For Legal Reasons" ); + + case 500: return "500" + _( "Internal Server Error" ); + case 501: return "501" + _( "Not Implemented" ); + case 502: return "502" + _( "Bad Gateway" ); + case 503: return "503" + _( "Service Unavailable" ); + case 504: return "504" + _( "Gateway Timeout" ); + case 505: return "505" + _( "HTTP Version Not Supported" ); + case 506: return "506" + _( "Variant Also Negotiates" ); + case 507: return "507" + _( "Insufficient Storag" ); + case 508: return "508" + _( "Loop Detecte" ); + case 510: return "510" + _( "Not Extended" ); + case 511: return "511" + _( "Network Authentication Required" ); + } + + return wxString::Format( _( "Code Unkonwn: %d" ), http_code ); +} \ No newline at end of file diff --git a/common/http_lib/http_lib_settings.cpp b/common/http_lib/http_lib_settings.cpp new file mode 100644 index 0000000000..afe516e144 --- /dev/null +++ b/common/http_lib/http_lib_settings.cpp @@ -0,0 +1,51 @@ +/* +* This program source code file is part of KiCad, a free EDA CAD application. +* +* Copyright (C) 2023 Andre F. K. Iwers +* +* 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 2 +* 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, you may find one here: +* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +* or you may search the http://www.gnu.org website for the version 2 license, +* or you may write to the Free Software Foundation, Inc., +* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +*/ + +#include + +#include +#include + +#include + +const int httplibSchemaVersion = 1; + + +HTTP_LIB_SETTINGS::HTTP_LIB_SETTINGS( const std::string& aFilename ) : + JSON_SETTINGS( aFilename, SETTINGS_LOC::NONE, httplibSchemaVersion ) +{ + m_params.emplace_back( new PARAM( "source.type", &sourceType, "" ) ); + + m_params.emplace_back( + new PARAM( "source.api_version", &m_Source.api_version, "" ) ); + + m_params.emplace_back( new PARAM( "source.root_url", &m_Source.root_url, "" ) ); + + m_params.emplace_back( new PARAM( "source.token", &m_Source.token, "" ) ); +} + + +wxString HTTP_LIB_SETTINGS::getFileExt() const +{ + return HTTPLibraryFileExtension; +} diff --git a/common/wildcards_and_files_ext.cpp b/common/wildcards_and_files_ext.cpp index 2ce481b3e9..8f62ece969 100644 --- a/common/wildcards_and_files_ext.cpp +++ b/common/wildcards_and_files_ext.cpp @@ -152,6 +152,7 @@ const std::string HtmlFileExtension( "html" ); const std::string EquFileExtension( "equ" ); const std::string HotkeyFileExtension( "hotkeys" ); const std::string DatabaseLibraryFileExtension( "kicad_dbl" ); +const std::string HTTPLibraryFileExtension( "kicad_httplib" ); const std::string ArchiveFileExtension( "zip" ); diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt index 58c0c31c6c..3b1badf301 100644 --- a/eeschema/CMakeLists.txt +++ b/eeschema/CMakeLists.txt @@ -395,6 +395,7 @@ set( EESCHEMA_SRCS sch_plugins/legacy/sch_legacy_plugin.cpp sch_plugins/legacy/sch_legacy_plugin_helpers.cpp sch_plugins/database/sch_database_plugin.cpp + sch_plugins/http_lib/sch_http_lib_plugin.cpp tools/assign_footprints.cpp tools/backannotate.cpp diff --git a/eeschema/sch_io_mgr.cpp b/eeschema/sch_io_mgr.cpp index 26cf43ae26..f0c3f2b201 100644 --- a/eeschema/sch_io_mgr.cpp +++ b/eeschema/sch_io_mgr.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include // for ExpandEnvVarSubstitutions #include @@ -72,6 +73,7 @@ SCH_PLUGIN* SCH_IO_MGR::FindPlugin( SCH_FILE_T aFileType ) case SCH_EASYEDA: return new SCH_EASYEDA_PLUGIN(); case SCH_EASYEDAPRO: return new SCH_EASYEDAPRO_PLUGIN(); case SCH_LTSPICE: return new SCH_LTSPICE_PLUGIN(); + case SCH_HTTP: return new SCH_HTTP_LIB_PLUGIN(); default: return nullptr; } } @@ -104,6 +106,7 @@ const wxString SCH_IO_MGR::ShowType( SCH_FILE_T aType ) case SCH_EASYEDA: return wxString( wxT( "EasyEDA (JLCEDA) Std" ) ); case SCH_EASYEDAPRO: return wxString( wxT( "EasyEDA (JLCEDA) Pro" ) ); case SCH_LTSPICE: return wxString( wxT( "LTspice" ) ); + case SCH_HTTP: return wxString( wxT( "HTTP" ) ); default: return wxString::Format( _( "Unknown SCH_FILE_T value: %d" ), aType ); } @@ -134,6 +137,8 @@ SCH_IO_MGR::SCH_FILE_T SCH_IO_MGR::EnumFromStr( const wxString& aType ) return SCH_EASYEDAPRO; else if( aType == wxT( "LTspice" ) ) return SCH_LTSPICE; + else if( aType == wxT( "HTTP" ) ) + return SCH_HTTP; // wxASSERT( blow up here ) diff --git a/eeschema/sch_io_mgr.h b/eeschema/sch_io_mgr.h index b23ed2452e..6a5fff1926 100644 --- a/eeschema/sch_io_mgr.h +++ b/eeschema/sch_io_mgr.h @@ -67,6 +67,7 @@ public: SCH_EASYEDA, ///< EasyEDA Std schematic file SCH_EASYEDAPRO, ///< EasyEDA Pro archive SCH_LTSPICE, ///< LtSpice Schematic format + SCH_HTTP, ///< KiCad HTTP library // Add your schematic type here. SCH_FILE_UNKNOWN diff --git a/eeschema/sch_plugins/http_lib/sch_http_lib_plugin.cpp b/eeschema/sch_plugins/http_lib/sch_http_lib_plugin.cpp new file mode 100644 index 0000000000..6cf589bb00 --- /dev/null +++ b/eeschema/sch_plugins/http_lib/sch_http_lib_plugin.cpp @@ -0,0 +1,450 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 Andre F. K. Iwers + * + * 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 "sch_http_lib_plugin.h" + + +SCH_HTTP_LIB_PLUGIN::SCH_HTTP_LIB_PLUGIN() : + m_libTable( nullptr ) +{ +} + + +SCH_HTTP_LIB_PLUGIN::~SCH_HTTP_LIB_PLUGIN() +{ +} + + +void SCH_HTTP_LIB_PLUGIN::EnumerateSymbolLib( wxArrayString& aSymbolNameList, + const wxString& aLibraryPath, + const STRING_UTF8_MAP* aProperties ) +{ + std::vector symbols; + EnumerateSymbolLib( symbols, aLibraryPath, aProperties ); + + for( LIB_SYMBOL* symbol : symbols ) + aSymbolNameList.Add( symbol->GetName() ); +} + + +void SCH_HTTP_LIB_PLUGIN::EnumerateSymbolLib( std::vector& aSymbolList, + const wxString& aLibraryPath, + const STRING_UTF8_MAP* aProperties ) +{ + wxCHECK_RET( m_libTable, _("httplib plugin missing library table handle!") ); + ensureSettings( aLibraryPath ); + ensureConnection(); + + if( !m_conn) + { + THROW_IO_ERROR( m_lastError ); + return; + } + + bool powerSymbolsOnly = + ( aProperties + && aProperties->find( SYMBOL_LIB_TABLE::PropPowerSymsOnly ) != aProperties->end() ); + + // clear buffer + m_cachedParts.clear(); + + for(const HTTP_LIB_CATEGORY& category : m_conn->getCategories() ) + { + std::vector found_parts; + + if( !m_conn->SelectAll( category, found_parts ) ) + { + if( !m_conn->GetLastError().empty() ) + { + wxString msg = wxString::Format( _( "Error retriving data from HTTP library %s: %s" ), + category.name, m_conn->GetLastError() ); + THROW_IO_ERROR( msg ); + } + + continue; + } + + // cache information for later use in LoadSymbol() + m_cachedParts.emplace( category.id, found_parts ); + + for( const HTTP_LIB_PART& part : found_parts ) + { + + wxString libIDString( part.name ); + + LIB_SYMBOL* symbol = loadSymbolFromPart( libIDString, category, part ); + + if( symbol && ( !powerSymbolsOnly || symbol->IsPower() ) ) + aSymbolList.emplace_back( symbol ); + + } + } +} + + +LIB_SYMBOL* SCH_HTTP_LIB_PLUGIN::LoadSymbol( const wxString& aLibraryPath, + const wxString& aAliasName, + const STRING_UTF8_MAP* aProperties ) +{ + wxCHECK( m_libTable, nullptr ); + ensureSettings( aLibraryPath ); + ensureConnection(); + + if( !m_conn ) + THROW_IO_ERROR( m_lastError ); + + std::string part_id = ""; + + std::string partName( aAliasName.ToUTF8() ); + + const HTTP_LIB_CATEGORY* foundCategory = nullptr; + HTTP_LIB_PART result; + + std::vector categories = m_conn->getCategories(); + + std::tuple relations = m_conn->getCachedParts()[partName]; + + // get the matching category + for( const HTTP_LIB_CATEGORY& categoryIter : categories ) + { + std::string associatedCatID = std::get<1>( relations ); + if( categoryIter.id == associatedCatID ) + { + foundCategory = &categoryIter; + + break; + } + } + + // return Null if no category was found. This should never happen + if( foundCategory == NULL ) + { + wxLogTrace( traceHTTPLib, wxT( "loadSymbol: no category found for %s" ), partName ); + + return NULL; + } + + // get the matching query ID + for( const auto& part : m_cachedParts[foundCategory->id] ) + { + if( part.id == std::get<0>( relations ) ) + { + part_id = part.id; + break; + } + } + + if( m_conn->SelectOne( part_id, result ) ) + { + wxLogTrace( traceHTTPLib, wxT( "loadSymbol: SelectOne (%s) found in %s" ), part_id, + foundCategory->name ); + } + else + { + wxLogTrace( traceHTTPLib, wxT( "loadSymbol: SelectOne (%s) failed for category %s" ), + part_id, foundCategory->name ); + + THROW_IO_ERROR( m_lastError ); + } + + wxCHECK( foundCategory, nullptr ); + + return loadSymbolFromPart( aAliasName, *foundCategory, result ); +} + + +void SCH_HTTP_LIB_PLUGIN::GetSubLibraryNames( std::vector& aNames ) +{ + ensureSettings( wxEmptyString ); + + aNames.clear(); + + std::set categoryNames; + + for( const HTTP_LIB_CATEGORY& categoryIter : m_conn->getCategories() ) + { + if( categoryNames.count( categoryIter.name ) ) + continue; + + aNames.emplace_back( categoryIter.name ); + categoryNames.insert( categoryIter.name ); + } +} + + +void SCH_HTTP_LIB_PLUGIN::GetAvailableSymbolFields( std::vector& aNames ) +{ + // TODO: Implement this sometime; This is currently broken... + std::copy( m_customFields.begin(), m_customFields.end(), std::back_inserter( aNames ) ); +} + + +void SCH_HTTP_LIB_PLUGIN::GetDefaultSymbolFields( std::vector& aNames ) +{ + std::copy( m_defaultShownFields.begin(), m_defaultShownFields.end(), + std::back_inserter( aNames ) ); +} + + +void SCH_HTTP_LIB_PLUGIN::ensureSettings( const wxString& aSettingsPath ) +{ + + auto tryLoad = [&]() + { + if( !m_settings->LoadFromFile() ) + { + wxString msg = wxString::Format( _( "HTTP library settings file %s missing or invalid" ), + aSettingsPath ); + + THROW_IO_ERROR( msg ); + } + + if( m_settings->m_Source.api_version.empty() ) + { + wxString msg = wxString::Format( + _( "HTTP library settings file %s is missing the API version number!" ), aSettingsPath ); + + THROW_IO_ERROR( msg ); + } + + if( m_settings->getSupportedAPIVersion() != m_settings->m_Source.api_version ) + { + wxString msg = wxString::Format( + _( "HTTP library settings file %s indicates API version conflict (Settings file: %s <-> KiCad: %s)!" ), + aSettingsPath, + m_settings->m_Source.api_version, + m_settings->getSupportedAPIVersion() ); + + THROW_IO_ERROR( msg ); + } + + if( m_settings->m_Source.root_url.empty() ) + { + wxString msg = wxString::Format( + _( "HTTP library settings file %s is missing the root URL!" ), + aSettingsPath ); + + THROW_IO_ERROR( msg ); + } + + // map lib source type + m_settings->m_Source.type = m_settings->get_HTTP_LIB_SOURCE_TYPE(); + if( m_settings->m_Source.type == HTTP_LIB_SOURCE_TYPE::INVALID ) + { + wxString msg = wxString::Format( + _( "HTTP library settings file has an invalid library type" ), aSettingsPath ); + + THROW_IO_ERROR( msg ); + } + + // make sure that the root url finishes with a forward slash + if( m_settings->m_Source.root_url.at( m_settings->m_Source.root_url.length() - 1 ) != '/' ) + { + m_settings->m_Source.root_url += "/"; + } + + // Append api version to root URL + m_settings->m_Source.root_url += m_settings->m_Source.api_version + "/"; + + }; + + if( !m_settings && !aSettingsPath.IsEmpty() ) + { + std::string path( aSettingsPath.ToUTF8() ); + m_settings = std::make_unique( path ); + + m_settings->SetReadOnly( true ); + + tryLoad(); + } + else if( m_settings ) + { + // If we have valid settings but no connection yet; reload settings in case user is editing + tryLoad(); + } + else if( !m_settings ) + { + wxLogTrace( traceHTTPLib, wxT( "ensureSettings: no settings available!" ) ); + } +} + + +void SCH_HTTP_LIB_PLUGIN::ensureConnection() +{ + wxCHECK_RET( m_settings, "Call ensureSettings before ensureConnection!" ); + + connect(); + + if( !m_conn || !m_conn->IsValidEnpoint() ) + { + wxString msg = wxString::Format( + _( "Could not connect to %s. Errors: %s" ), + m_settings->m_Source.root_url, m_lastError ); + + THROW_IO_ERROR( msg ); + } +} + + +void SCH_HTTP_LIB_PLUGIN::connect() +{ + wxCHECK_RET( m_settings, "Call ensureSettings before connect()!" ); + + if( !m_conn ) + { + + m_conn = std::make_unique( m_settings->m_Source, true ); + + if( !m_conn->IsValidEnpoint() ) + { + m_lastError = m_conn->GetLastError(); + return; + } + + } + +} + + +LIB_SYMBOL* SCH_HTTP_LIB_PLUGIN::loadSymbolFromPart( const wxString& aSymbolName, + const HTTP_LIB_CATEGORY& aCategory, + const HTTP_LIB_PART& aPart ) +{ + LIB_SYMBOL* symbol = nullptr; + LIB_SYMBOL* originalSymbol = nullptr; + LIB_ID symbolId; + + std::string symbolIdStr = aPart.symbolIdStr; + + // Get or Create the symbol using the found symbol + if( !symbolIdStr.empty() ) + { + symbolId.Parse( symbolIdStr ); + + if( symbolId.IsValid() ) + { + originalSymbol = m_libTable->LoadSymbol( symbolId ); + } + + if( originalSymbol ) + { + wxLogTrace( traceHTTPLib, wxT( "loadSymbolFromPart: found original symbol '%s'" ), + symbolIdStr ); + + symbol = originalSymbol->Duplicate(); + symbol->SetSourceLibId( symbolId ); + + symbol->LibId().SetSubLibraryName( aCategory.name ); + } + else if( !symbolId.IsValid() ) + { + wxLogTrace( traceHTTPLib, + wxT( "loadSymbolFromPart: source symbol id '%s' is invalid, " + "will create empty symbol" ), + symbolIdStr ); + } + else + { + wxLogTrace( traceHTTPLib, + wxT( "loadSymbolFromPart: source symbol '%s' not found, " + "will create empty symbol" ), + symbolIdStr ); + } + } + + if( !symbol ) + { + // Actual symbol not found: return metadata only; error will be + // indicated in the symbol chooser + symbol = new LIB_SYMBOL( aSymbolName ); + symbol->LibId().SetSubLibraryName( aCategory.name ); + } + + LIB_FIELD* field; + + for( auto& _field : aPart.fields ) + { + std::string fieldName = _field.first; + std::tuple fieldProperties = _field.second; + + if( fieldName == footprint_field ) + { + field = &symbol->GetFootprintField(); + field->SetText( std::get<0>( fieldProperties ) ); + field->SetVisible( std::get<1>( fieldProperties ) ); + } + else if( fieldName == description_field ) + { + field = &symbol->GetDescriptionField(); + field->SetText( std::get<0>( fieldProperties ) ); + field->SetVisible( std::get<1>( fieldProperties ) ); + } + else if( fieldName == value_field ) + { + field = &symbol->GetValueField(); + field->SetText( std::get<0>( fieldProperties ) ); + field->SetVisible( std::get<1>( fieldProperties ) ); + } + else if( fieldName == datasheet_field ) + { + field = &symbol->GetDatasheetField(); + field->SetText( std::get<0>( fieldProperties ) ); + field->SetVisible( std::get<1>( fieldProperties ) ); + } + else if( fieldName == reference_field ) + { + field = &symbol->GetReferenceField(); + field->SetText( std::get<0>( fieldProperties ) ); + field->SetVisible( std::get<1>( fieldProperties ) ); + } + else if( fieldName == keywords_field ) + { + symbol->SetKeyWords( std::get<0>( fieldProperties ) ); + } + else + { + // Generic fields + field = new LIB_FIELD( symbol->GetNextAvailableFieldId() ); + field->SetName( fieldName ); + + field->SetText( std::get<0>( fieldProperties ) ); + field->SetVisible( std::get<1>( fieldProperties ) ); + symbol->AddField( field ); + + m_customFields.insert( fieldName ); + } + } + + return symbol; +} + + +void SCH_HTTP_LIB_PLUGIN::SaveSymbol( const wxString& aLibraryPath, const LIB_SYMBOL* aSymbol, + const STRING_UTF8_MAP* aProperties ) +{ + // TODO: Implement this sometime; +} \ No newline at end of file diff --git a/eeschema/sch_plugins/http_lib/sch_http_lib_plugin.h b/eeschema/sch_plugins/http_lib/sch_http_lib_plugin.h new file mode 100644 index 0000000000..2314d7a4bf --- /dev/null +++ b/eeschema/sch_plugins/http_lib/sch_http_lib_plugin.h @@ -0,0 +1,130 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 Andre F. K. Iwers + * + * 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 . + */ + + +#ifndef KICAD_SCH_HTTP_LIB_PLUGIN_H +#define KICAD_SCH_HTTP_LIB_PLUGIN_H + +#include "http_lib/http_lib_settings.h" +#include + +#include +#include + +/** + * A KiCad HTTP library provides both symbol and footprint metadata, so there are "shim" plugins + * on both the symbol and footprint side of things that expose the database contents to the + * schematic and board editors. The architecture of these is slightly different from the other + * plugins because the backing file is just a configuration file rather than something that + * contains symbol or footprint data. + */ +class SCH_HTTP_LIB_PLUGIN : public SCH_PLUGIN +{ +public: + + SCH_HTTP_LIB_PLUGIN(); + virtual ~SCH_HTTP_LIB_PLUGIN(); + + const wxString GetName() const override + { + return wxT( "HTTP library" ); + } + + const PLUGIN_FILE_DESC GetLibraryFileDesc() const override + { + return PLUGIN_FILE_DESC( _HKI( "KiCad HTTP library files" ), + { HTTPLibraryFileExtension } ); + } + + int GetModifyHash() const override { return 0; } + + void EnumerateSymbolLib( wxArrayString& aSymbolNameList, + const wxString& aLibraryPath, + const STRING_UTF8_MAP* aProperties = nullptr ) override; + + void EnumerateSymbolLib( std::vector& aSymbolList, + const wxString& aLibraryPath, + const STRING_UTF8_MAP* aProperties = nullptr ) override; + + LIB_SYMBOL* LoadSymbol( const wxString& aLibraryPath, const wxString& aAliasName, + const STRING_UTF8_MAP* aProperties = nullptr ) override; + + bool SupportsSubLibraries() const override { return true; } + + void GetSubLibraryNames( std::vector& aNames ) override; + + void GetAvailableSymbolFields( std::vector& aNames ) override; + + void GetDefaultSymbolFields( std::vector& aNames ) override; + + bool IsSymbolLibWritable( const wxString& aLibraryPath ) override + { + // TODO: HTTP libraries are well capabale of supporting this. + return false; + } + + void SetLibTable( SYMBOL_LIB_TABLE* aTable ) override + { + m_libTable = aTable; + } + + + HTTP_LIB_SETTINGS* Settings() const { return m_settings.get(); } + + void SaveSymbol( const wxString& aLibraryPath, const LIB_SYMBOL* aSymbol, + const STRING_UTF8_MAP* aProperties = nullptr ) override; + +private: + + void ensureSettings( const wxString& aSettingsPath ); + + void ensureConnection(); + + void connect(); + + LIB_SYMBOL* loadSymbolFromPart( const wxString& aSymbolName, + const HTTP_LIB_CATEGORY& aCategory, + const HTTP_LIB_PART& aPart ); + + SYMBOL_LIB_TABLE* m_libTable; + + std::map> m_cachedParts; + + /// Generally will be null if no valid connection is established + std::unique_ptr m_conn; + + std::unique_ptr m_settings; + + std::set m_customFields; + + std::set m_defaultShownFields; + + wxString m_lastError; + + std::string symbol_field = "symbol"; + std::string footprint_field = "footprint"; + std::string description_field = "description"; + std::string keywords_field = "keywords"; + std::string value_field = "value"; + std::string datasheet_field = "datasheet"; + std::string reference_field = "reference"; + +}; + +#endif //KICAD_SCH_HTTP_LIB_PLUGIN_H diff --git a/include/http_lib/http_lib_connection.h b/include/http_lib/http_lib_connection.h new file mode 100644 index 0000000000..1353993c78 --- /dev/null +++ b/include/http_lib/http_lib_connection.h @@ -0,0 +1,121 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 Andre F. K. Iwers + * + * 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 . + */ + +#ifndef KICAD_HTTP_LIB_CONNECTION_H +#define KICAD_HTTP_LIB_CONNECTION_H + +#include +#include + +#include "http_lib/http_lib_settings.h" +#include + +extern const char* const traceHTTPLib; + + +class HTTP_LIB_CONNECTION +{ +public: + static const long DEFAULT_TIMEOUT = 10; + + HTTP_LIB_CONNECTION( const HTTP_LIB_SOURCE& aSource, bool aTestConnectionNow ); + + ~HTTP_LIB_CONNECTION(); + + bool IsValidEnpoint() const; + + + /** + * Retrieves a single part with full details from the HTTP library. + * @param aPk is the primary key of the part + * @param aResult will conatain the part if one was found + * @return true if aResult was filled; false otherwise + */ + bool SelectOne( const std::string aPartID, HTTP_LIB_PART& aFetchedPart ); + + /** + * Retrieves all parts from a specific category from the HTTP library. + * @param aPk is the primary key of the category + * @param aResults will be filled with all parts in that category + * @return true if the query succeeded and at least one part was found, false otherwise + */ + bool SelectAll( const HTTP_LIB_CATEGORY& aCategory, std::vector& aParts ); + + std::string GetLastError() const { return m_lastError; } + + std::vector getCategories() const { return m_categories; } + + auto getCachedParts() { return m_cache; } + +private: + + // This is clunky but at the moment the only way to free the pointer after use without KiCad crashing. + // at this point we can't use smart pointers as there is a problem with the order of how things are deleted/freed + std::unique_ptr createCurlEasyObject() + { + + std::unique_ptr aCurl( new KICAD_CURL_EASY() ); + + //KICAD_CURL_EASY* aCurl = new KICAD_CURL_EASY(); + + // prepare curl + aCurl->SetHeader( "Accept", "application/json" ); + aCurl->SetHeader( "Authorization", "Token " + m_token ); + + return aCurl; + } + + bool ValidateHTTPLibraryEndpoints(); + + bool syncCategories(); + + bool checkServerResponse( std::unique_ptr& m_curl ); + + bool boolFromString( const std::any& aVal ); + + wxString httpErrorCodeDescription( uint16_t http_code ); + + std::string m_token; + std::string m_rootURL; + std::string m_user_name; + std::string m_user_pass; + + HTTP_LIB_SOURCE_TYPE m_sourceType; + + HTTP_LIB_PART m_cached_part; + + // part.name part.id category.id + std::map> m_cache; + + bool m_enpointValid = false; + + std::string m_lastError; + + std::vector m_categories; + + std::map m_parts; + + const std::string http_endpoint_categories = "categories"; + const std::string http_endpoint_parts = "parts"; + const std::string http_endpoint_settings = "settings"; + const std::string http_endpoint_auth = "authentication"; + +}; + +#endif //KICAD_HTTP_LIB_CONNECTION_H diff --git a/include/http_lib/http_lib_settings.h b/include/http_lib/http_lib_settings.h new file mode 100644 index 0000000000..44dbbc181f --- /dev/null +++ b/include/http_lib/http_lib_settings.h @@ -0,0 +1,86 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 Andre F. K. Iwers + * + * 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 . + */ + +#ifndef KICAD_HTTP_LIB_SETTINGS_H +#define KICAD_HTTP_LIB_SETTINGS_H + +#include + + +enum class HTTP_LIB_SOURCE_TYPE +{ + REST_API, + INVALID +}; + + +struct HTTP_LIB_SOURCE +{ + HTTP_LIB_SOURCE_TYPE type; + std::string root_url; + std::string api_version; + std::string token; +}; + + +struct HTTP_LIB_CATEGORY +{ + std::string id; ///< id of category + std::string name; ///< name of category +}; + +struct HTTP_LIB_PART +{ + std::string id; + std::string name; + std::string symbolIdStr; + + std::map> fields; ///< additional generic fields +}; + +class HTTP_LIB_SETTINGS : public JSON_SETTINGS +{ +public: + HTTP_LIB_SETTINGS( const std::string& aFilename ); + + virtual ~HTTP_LIB_SETTINGS() {} + + HTTP_LIB_SOURCE m_Source; + + HTTP_LIB_SOURCE_TYPE get_HTTP_LIB_SOURCE_TYPE() + { + if( sourceType.compare( "REST_API" ) == 0 ) + { + return HTTP_LIB_SOURCE_TYPE::REST_API; + } + + return HTTP_LIB_SOURCE_TYPE::INVALID; + } + + std::string getSupportedAPIVersion() { return api_version; } + +protected: + wxString getFileExt() const override; + +private: + std::string sourceType; + std::string api_version = "v1"; +}; + +#endif //KICAD_HTTP_LIB_SETTINGS_H diff --git a/include/wildcards_and_files_ext.h b/include/wildcards_and_files_ext.h index f99a297b90..60d5ffed8d 100644 --- a/include/wildcards_and_files_ext.h +++ b/include/wildcards_and_files_ext.h @@ -136,6 +136,7 @@ extern const std::string HtmlFileExtension; extern const std::string EquFileExtension; extern const std::string HotkeyFileExtension; extern const std::string DatabaseLibraryFileExtension; +extern const std::string HTTPLibraryFileExtension; extern const std::string ArchiveFileExtension;