MDL-71096 core: Add meta information about APIs to core

Right now we have the information only in docs:
  - https://docs.moodle.org/dev/Core_APIs
  - https://moodledev.io/docs/apis

And, in fact, we are crawling those pages to get the information
from various tools (moodlecheck, CiBoT...). Obviously, that's far
from ideal, the source only has the current list of APIs, and
there isn't much information there but the names.

So we are moving the source of information to be in core, so it
can be modified between branches, and contains richer information:
 - The component the API belongs to, usually a subsystem or core.
 - If the API can be used as level 2 namespace.
 - If the API can be used as level 2 namespace out from its component.

Note that all that information has NO USES right now in core (and maybe
never will), but tools/checkers will benefit enormously by having that
information at hand, so we can check for namespaces, categories and
other bits way better.

Also, once we have this, the APIs dev documents linked above, surely
can be improved by being automatically generated and include all the
meta-information available.

It also includes a very basic json schema validating the basis. It can
be tried online @ https://www.jsonschemavalidator.net , or any other
tool. PHP requires extra libraries to be able to perform the validation.

Covered with unit tests, both api-related functions and structure validation.
This commit is contained in:
Eloy Lafuente (stronk7) 2022-10-19 11:46:10 +02:00
parent 40a89d8a9a
commit 28937d4243
5 changed files with 401 additions and 9 deletions

257
lib/apis.json Normal file
View File

@ -0,0 +1,257 @@
{
"access": {
"component": "core_access",
"allowedlevel2": true,
"allowedspread": false
},
"admin": {
"component": "core_admin",
"allowedlevel2": false,
"allowedspread": false
},
"adminpresets": {
"component": "core_adminpresets",
"allowedlevel2": true,
"allowedspread": false
},
"analytics": {
"component": "core_analytics",
"allowedlevel2": true,
"allowedspread": true
},
"availability": {
"component": "core_availability",
"allowedlevel2": false,
"allowedspread": false
},
"backup": {
"component": "core_backup",
"allowedlevel2": true,
"allowedspread": true
},
"badges": {
"component": "core_badges",
"allowedlevel2": false,
"allowedspread": false
},
"cache": {
"component": "core_cache",
"allowedlevel2": true,
"allowedspread": true
},
"calendar": {
"component": "core_calendar",
"allowedlevel2": false,
"allowedspread": false
},
"check": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"comment": {
"component": "core_comment",
"allowedlevel2": false,
"allowedspread": false
},
"competency": {
"component": "core_competency",
"allowedlevel2": false,
"allowedspread": false
},
"completion": {
"component": "core_completion",
"allowedlevel2": true,
"allowedspread": true
},
"core": {
"component": null,
"allowedlevel2": false,
"allowedspread": false
},
"ddl": {
"component": "core",
"allowedlevel2": true,
"allowedspread": false
},
"dml": {
"component": "core",
"allowedlevel2": true,
"allowedspread": false
},
"enrol": {
"component": "core_enrol",
"allowedlevel2": false,
"allowedspread": false
},
"event": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"external": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"files": {
"component": "core_files",
"allowedlevel2": true,
"allowedspread": false
},
"form": {
"component": "core_form",
"allowedlevel2": true,
"allowedspread": true
},
"grade": {
"component": "core_grade",
"allowedlevel2": false,
"allowedspread": false
},
"grading": {
"component": "core_grading",
"allowedlevel2": false,
"allowedspread": false
},
"group": {
"component": "core_group",
"allowedlevel2": false,
"allowedspread": false
},
"h5p": {
"component": "core_h5p",
"allowedlevel2": true,
"allowedspread": true
},
"lock": {
"component": "core",
"allowedlevel2": true,
"allowedspread": false
},
"log": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"media": {
"component": "core_media",
"allowedlevel2": false,
"allowedspread": false
},
"message": {
"component": "core_message",
"allowedlevel2": true,
"allowedspread": true
},
"navigation": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"oauth2": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"output": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"page": {
"component": "core",
"allowedlevel2": false,
"allowedspread": false
},
"payment": {
"component": "core_payment",
"allowedlevel2": true,
"allowedspread": true
},
"plagiarism": {
"component": "core_plagiarism",
"allowedlevel2": false,
"allowedspread": false
},
"portfolio": {
"component": "core_portfolio",
"allowedlevel2": false,
"allowedspread": false
},
"preference": {
"component": "core",
"allowedlevel2": false,
"allowedspread": false
},
"privacy": {
"component": "core_privacy",
"allowedlevel2": true,
"allowedspread": true
},
"question": {
"component": "core_question",
"allowedlevel2": true,
"allowedspread": true
},
"rating": {
"component": "core_rating",
"allowedlevel2": false,
"allowedspread": false
},
"reportbuilder": {
"component": "core_reportbuilder",
"allowedlevel2": true,
"allowedspread": true
},
"rss": {
"component": "core_rss",
"allowedlevel2": false,
"allowedspread": false
},
"search": {
"component": "core_search",
"allowedlevel2": true,
"allowedspread": true
},
"string": {
"component": "core",
"allowedlevel2": false,
"allowedspread": false
},
"tag": {
"component": "core_tag",
"allowedlevel2": false,
"allowedspread": false
},
"task": {
"component": "core",
"allowedlevel2": true,
"allowedspread": true
},
"test": {
"component": "core",
"allowedlevel2": false,
"allowedspread": false
},
"time": {
"component": "core",
"allowedlevel2": false,
"allowedspread": false
},
"upgrade": {
"component": "core",
"allowedlevel2": true,
"allowedspread": false
},
"webservice": {
"component": "core_webservice",
"allowedlevel2": false,
"allowedspread": false
},
"xapi": {
"component": "core_xapi",
"allowedlevel2": true,
"allowedspread": true
}
}

30
lib/apis.schema.json Normal file
View File

@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://moodle.org/apis.schema.json",
"title": "APIs",
"description": "Moodle valid APIs",
"type": "object",
"patternProperties": {
"^[a-z][a-z0-9]+$": {
"type": "object",
"properties": {
"component": {
"description": "Component the API belongs to, usually a subsystem or core. Null for the 'core' API itself",
"type": [ "string", "null" ],
"pattern": "^(core|[a-z][a-z0-9_]+)$"
},
"allowedlevel2": {
"description": "Can the API be used as level 2 namespace",
"type": "boolean"
},
"allowedspread": {
"description": "Can the API be used out from its own component",
"type": "boolean"
}
},
"minProperties": 3,
"maxProperties": 3,
"additionalProperties": false
}
}
}

View File

@ -72,6 +72,8 @@ class core_component {
protected static $parents = null;
/** @var array subplugins */
protected static $subplugins = null;
/** @var array cache of core APIs */
protected static $apis = null;
/** @var array list of all known classes that can be autoloaded */
protected static $classmap = null;
/** @var array list of all classes that have been renamed to be autoloaded */
@ -256,6 +258,7 @@ class core_component {
self::$subsystems = $cache['subsystems'];
self::$parents = $cache['parents'];
self::$subplugins = $cache['subplugins'];
self::$apis = $cache['apis'];
self::$classmap = $cache['classmap'];
self::$classmaprenames = $cache['classmaprenames'];
self::$filemap = $cache['filemap'];
@ -296,6 +299,7 @@ class core_component {
self::$subsystems = $cache['subsystems'];
self::$parents = $cache['parents'];
self::$subplugins = $cache['subplugins'];
self::$apis = $cache['apis'];
self::$classmap = $cache['classmap'];
self::$classmaprenames = $cache['classmaprenames'];
self::$filemap = $cache['filemap'];
@ -382,6 +386,7 @@ class core_component {
'plugins' => self::$plugins,
'parents' => self::$parents,
'subplugins' => self::$subplugins,
'apis' => self::$apis,
'classmap' => self::$classmap,
'classmaprenames' => self::$classmaprenames,
'filemap' => self::$filemap,
@ -406,6 +411,8 @@ $cache = '.var_export($cache, true).';
self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
}
self::$apis = self::fetch_apis();
self::fill_classmap_cache();
self::fill_classmap_renames_cache();
self::fill_filemap_cache();
@ -455,6 +462,14 @@ $cache = '.var_export($cache, true).';
return $info;
}
/**
* Returns list of core APIs.
* @return stdClass[]
*/
protected static function fetch_apis() {
return (array) json_decode(file_get_contents(__DIR__ . '/../apis.json'));
}
/**
* Returns list of known plugin types.
* @return array
@ -751,6 +766,22 @@ $cache = '.var_export($cache, true).';
return self::$subsystems;
}
/**
* List all core APIs and their attributes.
*
* This is a list of all the existing / allowed APIs in moodle, each one with the
* following attributes:
* - component: the component, usually a subsystem or core, the API belongs to.
* - allowedlevel2: if the API is allowed as level2 namespace or no.
* - allowedspread: if the API can spread out from its component or no.
*
* @return stdClass[] array of APIs (as keys) with their attributes as object instances.
*/
public static function get_core_apis() {
self::init();
return self::$apis;
}
/**
* Get list of available plugin types together with their location.
*
@ -1176,6 +1207,16 @@ $cache = '.var_export($cache, true).';
return isset(self::$subsystems[$subsystemname]);
}
/**
* Return true if apiname is a core API.
*
* @param string $apiname name of the API.
* @return bool true if core API.
*/
public static function is_core_api($apiname) {
return isset(self::$apis[$apiname]);
}
/**
* Records all class renames that have been made to facilitate autoloading.
*/
@ -1283,4 +1324,13 @@ $cache = '.var_export($cache, true).';
}
return $componentnames;
}
/**
* Returns the list of available API names.
*
* @return string[] the list of available API names.
*/
public static function get_core_api_names(): array {
return array_keys(self::get_core_apis());
}
}

View File

@ -18,16 +18,11 @@
* core_component related tests.
*
* @package core
* @category phpunit
* @category test
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Class core_component_testcase.
*
* @covers \core_component
*/
class component_test extends advanced_testcase {
@ -843,4 +838,64 @@ class component_test extends advanced_testcase {
$this->assertContains('tool_usertours', $componentnames);
$this->assertContains('core_favourites', $componentnames);
}
/**
* Basic tests for APIs related functions in the core_component class.
*/
public function test_apis_methods() {
$apis = core_component::get_core_apis();
$this->assertIsArray($apis);
$apinames = core_component::get_core_api_names();
$this->assertIsArray($apis);
// Both should return the very same APIs.
$this->assertEquals($apinames, array_keys($apis));
$this->assertFalse(core_component::is_core_api('lalala'));
$this->assertTrue(core_component::is_core_api('privacy'));
}
/**
* Test that the apis.json structure matches expectations
*
* While we include an apis.schema.json file in core, there isn't any PHP built-in allowing us
* to validate it (3rd part libraries needed). Plus the schema doesn't allow to validate things
* like uniqueness or sorting. We are going to do all that here.
*/
public function test_apis_json_validation() {
$apis = $sortedapis = core_component::get_core_apis();
ksort($sortedapis); // We'll need this later.
// General structure validations.
$this->assertIsArray($apis);
$this->assertGreaterThan(25, count($apis));
$this->assertArrayHasKey('privacy', $apis); // Verify a few.
$this->assertArrayHasKey('external', $apis);
$this->assertArrayHasKey('search', $apis);
$this->assertEquals(array_keys($sortedapis), array_keys($apis)); // Verify json is sorted alphabetically.
// Iterate over all apis and perform more validations.
foreach ($apis as $apiname => $attributes) {
// Message, to be used later and easier finding the problem.
$message = "Validation problem found with API: ${apiname}";
$this->assertIsObject($attributes, $message);
$this->assertMatchesRegularExpression('/^[a-z][a-z0-9]+$/', $apiname, $message);
$this->assertEquals(['component', 'allowedlevel2', 'allowedspread'], array_keys((array)$attributes), $message);
// Verify attributes.
if ($apiname !== 'core') { // Exception for core api, it doesn't have component.
$this->assertMatchesRegularExpression('/^(core|[a-z][a-z0-9_]+)$/', $attributes->component, $message);
} else {
$this->assertNull($attributes->component, $message);
}
$this->assertIsBool($attributes->allowedlevel2, $message);
$this->assertIsBool($attributes->allowedspread, $message);
// Cannot spread if level2 is not allowed.
$this->assertLessThanOrEqual($attributes->allowedlevel2, $attributes->allowedspread, $message);
}
}
}

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022120900.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2022121000.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.2dev (Build: 20221209)'; // Human-friendly version name