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#[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 #[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 #[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}