You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
9.4 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
  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. #include <magic_enum.hpp>
  21. #include <nlohmann/json.hpp>
  22. #include <wx/log.h>
  23. #include <wx/regex.h>
  24. #include <wx/stdstream.h>
  25. #include <wx/wfstream.h>
  26. #include <api/api_plugin.h>
  27. #include <api/api_plugin_manager.h>
  28. #include <json_conversions.h>
  29. bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
  30. {
  31. // TODO move to tl::expected and give user feedback about parse errors
  32. try
  33. {
  34. type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(),
  35. magic_enum::case_insensitive )
  36. .value_or( PLUGIN_RUNTIME_TYPE::INVALID );
  37. }
  38. catch( ... )
  39. {
  40. return false;
  41. }
  42. return type != PLUGIN_RUNTIME_TYPE::INVALID;
  43. }
  44. struct API_PLUGIN_CONFIG
  45. {
  46. API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile );
  47. bool valid;
  48. wxString identifier;
  49. wxString name;
  50. wxString description;
  51. PLUGIN_RUNTIME runtime;
  52. std::vector<PLUGIN_ACTION> actions;
  53. API_PLUGIN& parent;
  54. };
  55. API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile ) :
  56. parent( aParent )
  57. {
  58. valid = false;
  59. if( !aConfigFile.IsFileReadable() )
  60. return;
  61. wxLogTrace( traceApi, "Plugin: parsing config file" );
  62. wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) );
  63. wxStdInputStream fstream( fp );
  64. nlohmann::json js;
  65. try
  66. {
  67. js = nlohmann::json::parse( fstream, nullptr,
  68. /* allow_exceptions = */ true,
  69. /* ignore_comments = */ true );
  70. }
  71. catch( ... )
  72. {
  73. wxLogTrace( traceApi, "Plugin: exception during parse" );
  74. return;
  75. }
  76. // TODO add schema and validate
  77. // All of these are required; any exceptions here leave us with valid == false
  78. try
  79. {
  80. identifier = js.at( "identifier" ).get<wxString>();
  81. name = js.at( "name" ).get<wxString>();
  82. description = js.at( "description" ).get<wxString>();
  83. if( !runtime.FromJson( js.at( "runtime" ) ) )
  84. {
  85. wxLogTrace( traceApi, "Plugin: error parsing runtime section" );
  86. return;
  87. }
  88. }
  89. catch( ... )
  90. {
  91. wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
  92. return;
  93. }
  94. // At minimum, we need a reverse-DNS style identifier with two dots and a 2+ character TLD
  95. wxRegEx identifierRegex( wxS( "[\\w\\d]{2,}\\.[\\w\\d]+\\.[\\w\\d]+" ) );
  96. if( !identifierRegex.Matches( identifier ) )
  97. {
  98. wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
  99. identifier ) );
  100. return;
  101. }
  102. wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
  103. try
  104. {
  105. const nlohmann::json& actionsJs = js.at( "actions" );
  106. if( actionsJs.is_array() )
  107. {
  108. for( const nlohmann::json& actionJs : actionsJs )
  109. {
  110. if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
  111. {
  112. a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
  113. wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
  114. a->identifier ) );
  115. actions.emplace_back( *a );
  116. }
  117. }
  118. }
  119. }
  120. catch( ... )
  121. {
  122. wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
  123. }
  124. valid = true;
  125. }
  126. API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile ) :
  127. m_configFile( aConfigFile ),
  128. m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile ) )
  129. {
  130. }
  131. API_PLUGIN::~API_PLUGIN()
  132. {
  133. }
  134. bool API_PLUGIN::IsOk() const
  135. {
  136. return m_config->valid;
  137. }
  138. const wxString& API_PLUGIN::Identifier() const
  139. {
  140. return m_config->identifier;
  141. }
  142. const wxString& API_PLUGIN::Name() const
  143. {
  144. return m_config->name;
  145. }
  146. const wxString& API_PLUGIN::Description() const
  147. {
  148. return m_config->description;
  149. }
  150. const PLUGIN_RUNTIME& API_PLUGIN::Runtime() const
  151. {
  152. return m_config->runtime;
  153. }
  154. const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
  155. {
  156. return m_config->actions;
  157. }
  158. wxString API_PLUGIN::BasePath() const
  159. {
  160. return m_configFile.GetPath();
  161. }
  162. std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
  163. {
  164. // TODO move to tl::expected and give user feedback about parse errors
  165. PLUGIN_ACTION action( *this );
  166. try
  167. {
  168. action.identifier = aJson.at( "identifier" ).get<wxString>();
  169. wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
  170. action.name = aJson.at( "name" ).get<wxString>();
  171. action.description = aJson.at( "description" ).get<wxString>();
  172. action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
  173. action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
  174. }
  175. catch( ... )
  176. {
  177. wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
  178. return std::nullopt;
  179. }
  180. wxFileName f( action.entrypoint );
  181. if( !f.IsRelative() )
  182. {
  183. wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
  184. action.entrypoint ) );
  185. return std::nullopt;
  186. }
  187. f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
  188. if( !f.IsFileReadable() )
  189. {
  190. wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
  191. f.GetFullPath() ) );
  192. }
  193. if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
  194. {
  195. for( const nlohmann::json& argJs : aJson.at( "args" ) )
  196. {
  197. try
  198. {
  199. action.args.emplace_back( argJs.get<wxString>() );
  200. }
  201. catch( ... )
  202. {
  203. wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
  204. continue;
  205. }
  206. }
  207. }
  208. if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
  209. {
  210. for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
  211. {
  212. try
  213. {
  214. action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
  215. scopeJs.get<std::string>(), magic_enum::case_insensitive )
  216. .value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
  217. }
  218. catch( ... )
  219. {
  220. wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
  221. continue;
  222. }
  223. }
  224. }
  225. auto handleBitmap =
  226. [&]( const std::string& aKey, wxBitmapBundle& aDest )
  227. {
  228. if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
  229. {
  230. wxVector<wxBitmap> bitmaps;
  231. for( const nlohmann::json& iconJs : aJson.at( aKey ) )
  232. {
  233. wxFileName iconFile;
  234. try
  235. {
  236. iconFile = iconJs.get<wxString>();
  237. }
  238. catch( ... )
  239. {
  240. continue;
  241. }
  242. iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
  243. wxLogTrace( traceApi,
  244. wxString::Format( "Plugin: action %s: loading icon %s",
  245. action.identifier, iconFile.GetFullPath() ) );
  246. if( !iconFile.IsFileReadable() )
  247. {
  248. wxLogTrace( traceApi, "Plugin: icon file could not be read" );
  249. continue;
  250. }
  251. wxBitmap bmp;
  252. // TODO: If necessary; support types other than PNG
  253. bmp.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
  254. if( bmp.IsOk() )
  255. bitmaps.push_back( bmp );
  256. else
  257. wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
  258. }
  259. aDest = wxBitmapBundle::FromBitmaps( bitmaps );
  260. }
  261. };
  262. handleBitmap( "icons-light", action.icon_light );
  263. handleBitmap( "icons-dark", action.icon_dark );
  264. return action;
  265. }