Skip to main content

minco_core/
distribution.rs

1use crate::{
2    CapabilityProvision, CapabilityRequirement, ConfigurationField, DataClass,
3    HealthCheckDescriptor, IdleCostClass, MigrationSet, PluginId, PluginStability, ResourceKind,
4    WakeSource,
5};
6use semver::{Version, VersionReq};
7use serde::{Deserialize, Serialize};
8
9/// Archive-visible metadata used to evaluate a plugin before linking its code.
10///
11/// The plugin crate's `[package.metadata.minco]` table points to this record by
12/// package-root filename. Runtime composition still requires an ordinary Cargo
13/// dependency and an explicit typed constructor registration.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct PluginDistributionManifest {
17    pub schema: u32,
18    pub id: PluginId,
19    pub kind: PluginDistributionKind,
20    pub plugin_version: Version,
21    pub core_compatibility: VersionReq,
22    pub stability: PluginStability,
23    pub default_enabled: bool,
24    pub feature: String,
25    #[serde(default)]
26    pub runtimes: Vec<String>,
27    #[serde(default)]
28    pub databases: Vec<String>,
29    /// Other statically linked Minco plugins that must be registered.
30    #[serde(default)]
31    pub plugin_dependencies: Vec<PluginId>,
32    #[serde(default)]
33    pub requires: Vec<CapabilityRequirement>,
34    #[serde(default)]
35    pub provides: Vec<CapabilityProvision>,
36    #[serde(default)]
37    pub configuration: Vec<ConfigurationField>,
38    #[serde(default)]
39    pub operations: Vec<DistributionOperation>,
40    #[serde(default)]
41    pub migrations: Vec<MigrationSet>,
42    #[serde(default)]
43    pub seeds: Vec<DistributionSeed>,
44    #[serde(default)]
45    pub resources: Vec<DistributionResource>,
46    #[serde(default)]
47    pub health_checks: Vec<HealthCheckDescriptor>,
48    #[serde(default)]
49    pub data_classes: Vec<DataClass>,
50    pub retention: RetentionPolicy,
51    pub failure_policy: FailurePolicy,
52    pub documentation: DocumentationLinks,
53    pub conformance: ConformanceEvidence,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum PluginDistributionKind {
59    Plugin,
60    Adapter,
61    Runtime,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct DistributionOperation {
67    pub operation_id: String,
68    pub method: String,
69    pub path: String,
70    pub public: bool,
71    pub idempotent: bool,
72    #[serde(default)]
73    pub headers: Vec<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct DistributionSeed {
79    pub id: String,
80    pub database: String,
81    pub class: SeedClass,
82    pub path: String,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum SeedClass {
88    Reference,
89    Demo,
90    Test,
91    Bootstrap,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct DistributionResource {
97    pub id: String,
98    /// Cargo feature that activates this resource contract, when conditional.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub feature: Option<String>,
101    pub kind: ResourceKind,
102    pub idle_cost: IdleCostClass,
103    #[serde(default)]
104    pub wake_sources: Vec<WakeSource>,
105    #[serde(default)]
106    pub dependencies: Vec<String>,
107    #[serde(default)]
108    pub iam_actions: Vec<String>,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum RetentionPolicy {
114    None,
115    Ephemeral,
116    ApplicationDefined,
117    Persistent,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(deny_unknown_fields)]
122pub struct FailurePolicy {
123    pub mode: FailureMode,
124    pub description: String,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum FailureMode {
130    FailClosed,
131    Degrade,
132    BestEffort,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(deny_unknown_fields)]
137pub struct DocumentationLinks {
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub tutorial: Option<String>,
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub how_to: Option<String>,
142    pub reference: String,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub explanation: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(deny_unknown_fields)]
149pub struct ConformanceEvidence {
150    pub profile: String,
151    #[serde(default)]
152    pub evidence: Vec<String>,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn minimal_manifest() -> serde_json::Value {
160        serde_json::json!({
161            "schema": 1,
162            "id": "example",
163            "kind": "plugin",
164            "plugin_version": "0.1.0",
165            "core_compatibility": "*",
166            "stability": "experimental",
167            "default_enabled": false,
168            "feature": "plugin-example",
169            "runtimes": ["native"],
170            "retention": "none",
171            "failure_policy": {
172                "mode": "fail_closed",
173                "description": "Example failures are explicit."
174            },
175            "documentation": {"reference": "https://docs.rs/example"},
176            "conformance": {"profile": "minco-plugin-v1", "evidence": ["cargo test"]}
177        })
178    }
179
180    #[test]
181    fn nested_shared_contracts_reject_unknown_distribution_fields() {
182        let cases = [
183            (
184                "requires",
185                serde_json::json!({"name": "example", "version": "*", "typo": true}),
186            ),
187            (
188                "provides",
189                serde_json::json!({"name": "example", "version": "1.0.0", "typo": true}),
190            ),
191            (
192                "configuration",
193                serde_json::json!({
194                    "key": "mode",
195                    "kind": "string",
196                    "required": false,
197                    "secret": false,
198                    "description": "Example mode.",
199                    "typo": true
200                }),
201            ),
202            (
203                "migrations",
204                serde_json::json!({
205                    "id": "example",
206                    "database": "postgres",
207                    "path": "migrations/postgres",
208                    "typo": true
209                }),
210            ),
211            (
212                "health_checks",
213                serde_json::json!({"id": "example", "critical": true, "typo": true}),
214            ),
215        ];
216
217        for (field, value) in cases {
218            let mut manifest = minimal_manifest();
219            manifest[field] = serde_json::json!([value]);
220            assert!(
221                serde_json::from_value::<PluginDistributionManifest>(manifest).is_err(),
222                "unknown nested field was accepted in {field}"
223            );
224        }
225    }
226}