Commit b5eddf35 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

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

parent 896f0066
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -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",
+12 −12
Original line number Diff line number Diff line
@@ -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
@@ -162,13 +162,16 @@ 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]',
                'label'  => 'required|max_length[32]',
                'hint'   => 'permit_empty|max_length[128]',
                'helper' => 'permit_empty|max_length[128]',
            ];
            $defaultField = [
                'key'      => '',
                'name'        => '',
                'description' => '',
                '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'    => [
+191 −0
Original line number Diff line number Diff line
{
  "$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
    }
  }
}
+1 −1
Original line number Diff line number Diff line
@@ -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">
+5 −3
Original line number Diff line number Diff line
@@ -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; ?>