use crate::config::ConfigEntity;
use crate::error::{Error, Result};
use crate::paths;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ActionType {
Api,
Command,
Builtin,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum BuiltinAction {
CopyColumn,
ExportCsv,
CopyJson,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
#[default]
Get,
Post,
Put,
Patch,
Delete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployCapability {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub verifications: Vec<DeployVerification>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub overrides: Vec<DeployOverride>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub version_patterns: Vec<VersionPatternConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_tag: Option<SinceTagConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestMappingConfig {
pub source_dirs: Vec<String>,
pub test_dirs: Vec<String>,
pub test_file_pattern: String,
#[serde(default = "default_test_prefix")]
pub method_prefix: String,
#[serde(default)]
pub inline_tests: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub critical_patterns: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skip_test_patterns: Vec<String>,
}
fn default_test_prefix() -> String {
"test_".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditCapability {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ignore_claim_patterns: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub feature_patterns: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub feature_labels: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub doc_targets: HashMap<String, DocTarget>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub feature_context: HashMap<String, FeatureContextRule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_mapping: Option<TestMappingConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureContextRule {
#[serde(default)]
pub doc_comment: bool,
#[serde(default)]
pub block_fields: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocTarget {
pub file: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub heading: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutableCapability {
pub runtime: RuntimeConfig,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<InputConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<OutputConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformCapability {
#[serde(skip_serializing_if = "Option::is_none")]
pub config_schema: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default_pinned_files: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default_pinned_logs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub database: Option<DatabaseConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discovery: Option<DiscoveryConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidesConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub file_extensions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ScriptsConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refactor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topology: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionManifest {
#[serde(default, skip_serializing)]
pub id: String,
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub provides: Option<ProvidesConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts: Option<ScriptsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deploy: Option<DeployCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audit: Option<AuditCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub executable: Option<ExecutableCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<PlatformCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cli: Option<CliConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build: Option<BuildConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lint: Option<LintConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test: Option<TestConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<ActionConfig>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub hooks: HashMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub settings: Vec<SettingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires: Option<RequirementsConfig>,
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, serde_json::Value>,
#[serde(skip)]
pub extension_path: Option<String>,
}
impl ExtensionManifest {
pub fn has_cli(&self) -> bool {
self.cli.is_some()
}
pub fn has_build(&self) -> bool {
self.build.is_some()
}
pub fn has_lint(&self) -> bool {
self.lint
.as_ref()
.and_then(|c| c.extension_script.as_ref())
.is_some()
}
pub fn has_test(&self) -> bool {
self.test
.as_ref()
.and_then(|c| c.extension_script.as_ref())
.is_some()
}
pub fn lint_script(&self) -> Option<&str> {
self.lint
.as_ref()
.and_then(|c| c.extension_script.as_deref())
}
pub fn test_script(&self) -> Option<&str> {
self.test
.as_ref()
.and_then(|c| c.extension_script.as_deref())
}
pub fn deploy_verifications(&self) -> &[DeployVerification] {
self.deploy
.as_ref()
.map(|d| d.verifications.as_slice())
.unwrap_or(&[])
}
pub fn deploy_overrides(&self) -> &[DeployOverride] {
self.deploy
.as_ref()
.map(|d| d.overrides.as_slice())
.unwrap_or(&[])
}
pub fn version_patterns(&self) -> &[VersionPatternConfig] {
self.deploy
.as_ref()
.map(|d| d.version_patterns.as_slice())
.unwrap_or(&[])
}
pub fn since_tag(&self) -> Option<&SinceTagConfig> {
self.deploy.as_ref().and_then(|d| d.since_tag.as_ref())
}
pub fn runtime(&self) -> Option<&RuntimeConfig> {
self.executable.as_ref().map(|e| &e.runtime)
}
pub fn inputs(&self) -> &[InputConfig] {
self.executable
.as_ref()
.map(|e| e.inputs.as_slice())
.unwrap_or(&[])
}
pub fn audit_ignore_claim_patterns(&self) -> &[String] {
self.audit
.as_ref()
.map(|a| a.ignore_claim_patterns.as_slice())
.unwrap_or(&[])
}
pub fn audit_feature_patterns(&self) -> &[String] {
self.audit
.as_ref()
.map(|a| a.feature_patterns.as_slice())
.unwrap_or(&[])
}
pub fn audit_feature_labels(&self) -> &HashMap<String, String> {
static EMPTY: std::sync::LazyLock<HashMap<String, String>> =
std::sync::LazyLock::new(HashMap::new);
self.audit
.as_ref()
.map(|a| &a.feature_labels)
.unwrap_or(&EMPTY)
}
pub fn audit_doc_targets(&self) -> &HashMap<String, DocTarget> {
static EMPTY: std::sync::LazyLock<HashMap<String, DocTarget>> =
std::sync::LazyLock::new(HashMap::new);
self.audit
.as_ref()
.map(|a| &a.doc_targets)
.unwrap_or(&EMPTY)
}
pub fn audit_feature_context(&self) -> &HashMap<String, FeatureContextRule> {
static EMPTY: std::sync::LazyLock<HashMap<String, FeatureContextRule>> =
std::sync::LazyLock::new(HashMap::new);
self.audit
.as_ref()
.map(|a| &a.feature_context)
.unwrap_or(&EMPTY)
}
pub fn test_mapping(&self) -> Option<&TestMappingConfig> {
self.audit.as_ref().and_then(|a| a.test_mapping.as_ref())
}
pub fn database(&self) -> Option<&DatabaseConfig> {
self.platform.as_ref().and_then(|p| p.database.as_ref())
}
pub fn semver(&self) -> crate::error::Result<semver::Version> {
super::version::parse_extension_version(&self.version, &self.id)
}
pub fn provided_file_extensions(&self) -> &[String] {
self.provides
.as_ref()
.map(|p| p.file_extensions.as_slice())
.unwrap_or(&[])
}
pub fn provided_capabilities(&self) -> &[String] {
self.provides
.as_ref()
.map(|p| p.capabilities.as_slice())
.unwrap_or(&[])
}
pub fn handles_file_extension(&self, ext: &str) -> bool {
self.provided_file_extensions().iter().any(|e| e == ext)
}
pub fn fingerprint_script(&self) -> Option<&str> {
self.scripts.as_ref().and_then(|s| s.fingerprint.as_deref())
}
pub fn refactor_script(&self) -> Option<&str> {
self.scripts.as_ref().and_then(|s| s.refactor.as_deref())
}
pub fn topology_script(&self) -> Option<&str> {
self.scripts.as_ref().and_then(|s| s.topology.as_deref())
}
}
impl ConfigEntity for ExtensionManifest {
const ENTITY_TYPE: &'static str = "extension";
const DIR_NAME: &'static str = "extensions";
fn id(&self) -> &str {
&self.id
}
fn set_id(&mut self, id: String) {
self.id = id;
}
fn not_found_error(id: String, suggestions: Vec<String>) -> Error {
Error::extension_not_found(id, suggestions)
}
fn config_path(id: &str) -> Result<PathBuf> {
paths::extension_manifest(id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequirementsConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extensions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub cli: Option<DatabaseCliConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseCliConfig {
pub tables_command: String,
pub describe_command: String,
pub query_command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CliHelpConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub project_id_help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args_help: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliConfig {
pub tool: String,
pub display_name: String,
pub command_template: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_cli_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub working_dir_template: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub settings_flags: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<CliHelpConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoveryConfig {
pub find_command: String,
pub base_path_transform: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name_command: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployVerification {
pub path_pattern: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub verify_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verify_error_message: Option<String>,
}
fn default_staging_path() -> String {
"/tmp/homeboy-staging".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployOverride {
pub path_pattern: String,
#[serde(default = "default_staging_path")]
pub staging_path: String,
pub install_command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cleanup_command: Option<String>,
#[serde(default)]
pub skip_permissions_fix: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionPatternConfig {
pub extension: String,
pub pattern: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SinceTagConfig {
pub extensions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder_pattern: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifact_extensions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub script_names: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command_template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extension_script: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_build_script: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact_pattern: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cleanup_paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LintConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub extension_script: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub extension_script: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeConfig {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub runtime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub run_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub setup_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ready_check: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entrypoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_site: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Vec<String>>,
#[serde(rename = "playwrightBrowsers", skip_serializing_if = "Option::is_none")]
pub playwright_browsers: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputConfig {
pub id: String,
#[serde(rename = "type")]
pub input_type: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<SelectOption>>,
pub arg: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectOption {
pub value: String,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
pub schema: OutputSchema,
pub display: String,
pub selectable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionConfig {
pub id: String,
pub label: String,
#[serde(rename = "type")]
pub action_type: ActionType,
#[serde(skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<HttpMethod>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_auth: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub builtin: Option<BuiltinAction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingConfig {
pub id: String,
#[serde(rename = "type")]
pub setting_type: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
}