feat(plugins): add json schema definition for plugin manifest

This commit is contained in:
Yassine Doghri 2024-05-05 17:10:20 +00:00
parent 896f00661f
commit b5eddf351f
5 changed files with 216 additions and 17 deletions

View File

@ -30,7 +30,13 @@
"spark": "php",
"env": "dotenv",
".rsync-filter": "diff"
}
},
"json.schemas": [
{
"fileMatch": ["plugins/**/manifest.json"],
"url": "/workspaces/castopod/modules/Plugins/manifest.schema.json"
}
]
},
"extensions": [
"astro-build.astro-vscode",

View File

@ -53,7 +53,7 @@ abstract class BasePlugin implements PluginInterface
*/
public function __set(string $name, array|string $value): void
{
$this->{$name} = $name === 'releaseDate' ? Time::createFromFormat('Y-m-d', $value) : $value;
$this->{$name} = $value;
}
public function init(): void
@ -161,14 +161,17 @@ abstract class BasePlugin implements PluginInterface
if (array_key_exists('settings', $manifest)) {
$fieldRules = [
'key' => 'required|alpha_numeric',
'name' => 'required|max_length[32]',
'description' => 'permit_empty|max_length[128]',
'key' => 'required|alpha_numeric',
'label' => 'required|max_length[32]',
'hint' => 'permit_empty|max_length[128]',
'helper' => 'permit_empty|max_length[128]',
];
$defaultField = [
'key' => '',
'name' => '',
'description' => '',
'key' => '',
'label' => '',
'hint' => '',
'helper' => '',
'optional' => false,
];
$validation->setRules($fieldRules);
foreach ($manifest['settings'] as $key => $settings) {
@ -185,14 +188,12 @@ abstract class BasePlugin implements PluginInterface
$rules = [
'name' => 'required|max_length[32]',
'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]',
'compatible' => 'required|in_list[1.0]',
'description' => 'max_length[128]',
'releaseDate' => 'valid_date[Y-m-d]',
'license' => 'in_list[MIT]',
'author.name' => 'permit_empty|max_length[32]',
'author.email' => 'permit_empty|valid_email',
'author.url' => 'permit_empty|valid_url_strict',
'website' => 'valid_url_strict',
'homepage' => 'valid_url_strict',
'keywords.*' => 'permit_empty|in_list[seo,podcasting20,analytics]',
'hooks.*' => 'permit_empty|in_list[' . implode(',', Plugins::HOOKS) . ']',
'settings' => 'permit_empty',
@ -206,10 +207,9 @@ abstract class BasePlugin implements PluginInterface
$defaultAttributes = [
'description' => '',
'releaseDate' => '',
'license' => '',
'author' => [],
'website' => '',
'homepage' => '',
'hooks' => [],
'keywords' => [],
'settings' => [

View File

@ -0,0 +1,191 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "/schemas/manifest.json",
"title": "JSON schema for Castopod Plugins's manifest.json files",
"description": "The Castopod plugin manifest defines both metadata and behavior of a plugin",
"type": "object",
"properties": {
"name": {
"description": "The plugin name, including 'vendor-name/' prefix",
"type": "string",
"pattern": "^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9]([_.-]?[a-z0-9]+)*$",
"examples": ["acme/hello-world"]
},
"version": {
"description": "The plugin's semantic version. See https://semver.org/",
"type": "string",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"examples": ["1.0.0"]
},
"description": {
"description": "This helps people discover your plugin as it's listed in repositories",
"type": "string"
},
"author": {
"$ref": "#/$defs/person"
},
"authors": {
"type": "array",
"items": {
"$ref": "#/$defs/person"
}
},
"homepage": {
"description": "The URL to the plugin homepage",
"type": "string",
"format": "uri"
},
"license": {
"description": "You should specify a license for your plugin so that people know how they are permitted to use it, and any restrictions you're placing on it.",
"default": "UNLICENSED",
"anyOf": [
{
"type": "string"
},
{
"enum": [
"AGPL-3.0-only",
"AGPL-3.0-or-later",
"Apache-2.0",
"BSL-1.0",
"GPL-3.0-only",
"GPL-3.0-or-later",
"LGPL-3.0-only",
"LGPL-3.0-or-later",
"MIT",
"MPL-2.0",
"Unlicense",
"UNLICENSED"
]
}
]
},
"private": {
"type": "boolean",
"description": "If set to true, then repositories should refuse to publish it."
},
"keywords": {
"description": "This helps people discover your plugin as it's listed in repositories",
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"enum": [
"accessibility",
"analytics",
"monetization",
"podcasting2",
"privacy",
"seo"
]
}
]
},
"uniqueItems": true
},
"hooks": {
"description": "The hooks used by the plugin.",
"type": "array",
"items": {
"enum": ["channelTag", "itemTag", "siteHead"]
},
"uniqueItems": true
},
"settings": {
"type": "object",
"properties": {
"general": {
"type": "array",
"items": {
"$ref": "#/$defs/settings-field"
}
},
"podcast": {
"type": "array",
"items": {
"$ref": "#/$defs/settings-field"
}
},
"episode": {
"type": "array",
"items": {
"$ref": "#/$defs/settings-field"
}
}
}
},
"files": {
"description": "List of files to include in your plugin package. If you include a folder in the array, all files inside it will also be included.",
"type": "array",
"items": {
"type": "string"
}
},
"repository": {
"description": "Specify the place where your plugin code lives. This is helpful for people who want to contribute.",
"type": ["object", "string"],
"properties": {
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"directory": {
"type": "string"
}
}
}
},
"required": ["name", "version"],
"additionalProperties": false,
"$defs": {
"person": {
"description": "A person who has been involved in creating or maintaining this plugin.",
"type": ["object", "string"],
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
"settings-field": {
"type": "object",
"properties": {
"type": {
"enum": ["text", "email", "url", "markdown", "number", "switch"],
"default": "text"
},
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"hint": {
"type": "string"
},
"helper": {
"type": "string"
},
"optional": {
"type": "boolean"
}
},
"required": ["key", "label"],
"additionalProperties": false
}
}
}

View File

@ -12,7 +12,7 @@
<p class="mt-2 text-gray-600"><?= $plugin->getDescription() ?></p>
</div>
<footer class="flex items-center justify-between mt-4">
<a href="<?= $plugin->website ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
<a href="<?= $plugin->homepage ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
'class' => 'text-gray-500',
]) . lang('Plugins.website') ?></a>
<div class="flex gap-x-2">

View File

@ -2,9 +2,11 @@
<?= csrf_field() ?>
<?php foreach ($plugin->settings[$type] as $field): ?>
<Forms.Field
name="<?= $field['key'] ?>"
label="<?= $field['name'] ?>"
hint="<?= $field['description'] ?>"
name="<?= esc($field['key']) ?>"
label="<?= esc($field['label']) ?>"
hint="<?= esc($field['hint']) ?>"
helper="<?= esc($field['helper']) ?>"
required="<?= $field['optional'] === 'true' ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field['key'], $context) ?>"
/>
<?php endforeach; ?>