nxd_core/domain/contract/
resources.rs1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum Resource {
11 Machine(Machine),
12 DeploymentTarget(Box<DeploymentTarget>),
13 SyntheticResource(SyntheticResource),
14 ProviderOwned(ProviderOwnedResource),
15}
16
17#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
19#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
20enum FirstPartyResource {
21 Machine(Machine),
22 DeploymentTarget(Box<DeploymentTarget>),
23 SyntheticResource(SyntheticResource),
24}
25
26impl From<FirstPartyResource> for Resource {
27 fn from(value: FirstPartyResource) -> Self {
28 match value {
29 FirstPartyResource::Machine(v) => Self::Machine(v),
30 FirstPartyResource::DeploymentTarget(v) => Self::DeploymentTarget(v),
31 FirstPartyResource::SyntheticResource(v) => Self::SyntheticResource(v),
32 }
33 }
34}
35
36impl TryFrom<&Resource> for FirstPartyResource {
37 type Error = ();
38 fn try_from(value: &Resource) -> Result<Self, Self::Error> {
39 Ok(match value {
40 Resource::Machine(v) => Self::Machine(v.clone()),
41 Resource::DeploymentTarget(v) => Self::DeploymentTarget(v.clone()),
42 Resource::SyntheticResource(v) => Self::SyntheticResource(v.clone()),
43 Resource::ProviderOwned(_) => return Err(()),
44 })
45 }
46}
47
48impl Serialize for Resource {
49 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50 where
51 S: Serializer,
52 {
53 match self {
54 Resource::ProviderOwned(owned) => owned.desired.serialize(serializer),
55 other => {
56 FirstPartyResource::try_from(other).expect("first-party resource").serialize(serializer)
57 }
58 }
59 }
60}
61
62impl<'de> Deserialize<'de> for Resource {
63 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64 where
65 D: Deserializer<'de>,
66 {
67 let value = Value::deserialize(deserializer)?;
68 match serde_json::from_value::<FirstPartyResource>(value.clone()) {
69 Ok(first_party) => return Ok(first_party.into()),
70 Err(error)
71 if matches!(
72 value.get("kind").and_then(Value::as_str),
73 Some("machine" | "deploymentTarget" | "syntheticResource")
74 ) =>
75 {
76 return Err(serde::de::Error::custom(error));
77 }
78 Err(_) => {}
79 }
80 ProviderOwnedResource::from_wire_value(value)
81 .map(Resource::ProviderOwned)
82 .map_err(serde::de::Error::custom)
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ProviderOwnedResource {
91 #[serde(rename = "resourceKind")]
93 pub resource_kind: String,
94 pub id: String,
95 pub provider: String,
96 #[serde(default)]
97 pub depends_on: Vec<String>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub deployment_target: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub secret_binding: Option<String>,
102 pub desired: Value,
104}
105
106impl ProviderOwnedResource {
107 pub fn from_wire_value(value: Value) -> Result<Self, String> {
109 let object = value
110 .as_object()
111 .ok_or_else(|| "provider-owned resource must be a JSON object".to_string())?;
112 let resource_kind = object
113 .get("kind")
114 .and_then(Value::as_str)
115 .filter(|value| !value.is_empty())
116 .ok_or_else(|| "provider-owned resource is missing kind".to_string())?
117 .to_string();
118 let id = object
119 .get("id")
120 .and_then(Value::as_str)
121 .filter(|value| !value.is_empty())
122 .ok_or_else(|| "provider-owned resource is missing id".to_string())?
123 .to_string();
124 let provider = object
127 .get("provider")
128 .and_then(Value::as_str)
129 .filter(|value| !value.is_empty())
130 .ok_or_else(|| format!("provider-owned resource {id} is missing provider"))?
131 .to_string();
132 let depends_on = object
135 .get("dependsOn")
136 .and_then(Value::as_array)
137 .map(|items| items.iter().filter_map(Value::as_str).map(str::to_string).collect::<Vec<_>>())
138 .unwrap_or_default();
139 let deployment_target = object
141 .get("deploymentTarget")
142 .and_then(Value::as_str)
143 .filter(|value| !value.is_empty())
144 .map(str::to_string);
145 let secret_binding = object
146 .get("secretBinding")
147 .and_then(Value::as_str)
148 .filter(|value| !value.is_empty())
149 .map(str::to_string);
150 Ok(Self {
151 resource_kind,
152 id,
153 provider,
154 depends_on,
155 deployment_target,
156 secret_binding,
157 desired: value,
158 })
159 }
160
161 pub fn associated_deployment_target(&self) -> Option<String> {
162 if let Some(target) = &self.deployment_target
163 && !target.is_empty()
164 {
165 return Some(target.clone());
166 }
167 self
168 .desired
169 .get("deploymentTarget")
170 .and_then(Value::as_str)
171 .filter(|v| !v.is_empty())
172 .map(str::to_string)
173 }
174}
175
176#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
177#[serde(rename_all = "camelCase", deny_unknown_fields)]
178pub struct DeploymentTarget {
179 pub id: String,
180 #[serde(default)]
181 pub depends_on: Vec<String>,
182 #[serde(default)]
183 pub labels: BTreeMap<String, String>,
184 pub metadata: crate::domain::deployment_metadata::FlakeMetadata,
185}
186
187#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct Machine {
190 pub id: String,
191 #[serde(default)]
192 pub labels: BTreeMap<String, String>,
193}
194
195#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct SyntheticResource {
198 pub id: String,
199 pub provider: String,
200 pub desired: serde_json::Value,
201 #[serde(default)]
202 pub depends_on: Vec<String>,
203}
204
205pub fn resource_id(resource: &Resource) -> &str {
207 match resource {
208 Resource::Machine(value) => &value.id,
209 Resource::DeploymentTarget(value) => &value.id,
210 Resource::SyntheticResource(value) => &value.id,
211 Resource::ProviderOwned(value) => &value.id,
212 }
213}
214
215pub fn resource_provider(resource: &Resource) -> Option<&str> {
217 match resource {
218 Resource::SyntheticResource(value) => Some(&value.provider),
219 Resource::ProviderOwned(value) if !value.provider.is_empty() => Some(&value.provider),
220 _ => None,
221 }
222}
223
224pub fn resource_kind(resource: &Resource) -> Option<&str> {
226 match resource {
227 Resource::ProviderOwned(value) => Some(value.resource_kind.as_str()),
228 Resource::SyntheticResource(_) => Some("syntheticResource"),
229 Resource::DeploymentTarget(_) => Some("deploymentTarget"),
230 Resource::Machine(_) => Some("machine"),
231 }
232}
233
234pub fn resource_desired(resource: &Resource) -> Option<&Value> {
236 match resource {
237 Resource::ProviderOwned(value) => Some(&value.desired),
238 Resource::SyntheticResource(value) => Some(&value.desired),
239 _ => None,
240 }
241}
242
243pub fn resource_depends_on(resource: &Resource) -> &[String] {
245 match resource {
246 Resource::DeploymentTarget(value) => &value.depends_on,
247 Resource::SyntheticResource(value) => &value.depends_on,
248 Resource::ProviderOwned(value) => &value.depends_on,
249 _ => &[],
250 }
251}
252
253pub fn resource_associated_deployment_target(resource: &Resource) -> Option<String> {
255 match resource {
256 Resource::ProviderOwned(value) => value.associated_deployment_target(),
257 Resource::SyntheticResource(value) => value
258 .desired
259 .get("deploymentTarget")
260 .and_then(Value::as_str)
261 .filter(|v| !v.is_empty())
262 .map(str::to_string),
263 _ => None,
264 }
265}
266
267pub fn resource_secret_binding(resource: &Resource) -> Option<&str> {
269 match resource {
270 Resource::ProviderOwned(value) => value.secret_binding.as_deref(),
271 Resource::SyntheticResource(value) => {
272 value.desired.get("secretBinding").and_then(Value::as_str)
273 }
274 _ => None,
275 }
276}