1use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::Manifest;
9
10#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
12pub enum ContractIdentityError {
13 #[error(
15 "package identity `{stored_version}` predates `.v4` worker-contract commitment; re-deploy this package under `.v4`"
16 )]
17 RedeployRequired {
18 stored_version: String,
20 },
21}
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
25pub struct PackageContract {
26 #[serde(serialize_with = "crate::canonical::serialize_value")]
28 pub input_schema: Value,
29 #[serde(serialize_with = "crate::canonical::serialize_value")]
31 pub output_schema: Value,
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub workers: Vec<WorkerContract>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub children: Vec<ChildContract>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub signals: Vec<SignalContract>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub additional_workflows: Vec<AdditionalWorkflowContract>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub unscoped_activities: Vec<String>,
49}
50
51impl PackageContract {
52 #[must_use]
58 pub fn from_manifest(manifest: &Manifest) -> Self {
59 Self {
60 input_schema: manifest.input_schema.clone(),
61 output_schema: manifest.output_schema.clone(),
62 workers: Vec::new(),
63 children: Vec::new(),
64 signals: Vec::new(),
65 additional_workflows: manifest
66 .additional_workflows
67 .iter()
68 .map(|entry| AdditionalWorkflowContract {
69 workflow_type: entry.workflow_type.clone(),
70 input_schema: entry.input_schema.clone(),
71 output_schema: entry.output_schema.clone(),
72 })
73 .collect(),
74 unscoped_activities: manifest
75 .activities
76 .iter()
77 .map(|activity| activity.activity_type.clone())
78 .collect(),
79 }
80 }
81
82 #[must_use]
88 pub fn canonical_bytes(&self) -> Vec<u8> {
89 let mut bytes = Vec::new();
90 encode_json(&mut bytes, None, &self.input_schema);
91 encode_json(&mut bytes, None, &self.output_schema);
92
93 let mut workers = self
94 .workers
95 .iter()
96 .map(WorkerContract::canonical_bytes)
97 .collect::<Vec<_>>();
98 workers.sort();
99 encode_len(&mut bytes, workers.len());
100 for worker in workers {
101 update_record(&mut bytes, &worker);
102 }
103
104 let mut children = self
105 .children
106 .iter()
107 .map(ChildContract::canonical_bytes)
108 .collect::<Vec<_>>();
109 children.sort();
110 encode_len(&mut bytes, children.len());
111 for child in children {
112 update_record(&mut bytes, &child);
113 }
114
115 let mut signals = self.signals.iter().collect::<Vec<_>>();
116 signals.sort_by(|left, right| left.name.cmp(&right.name));
117 encode_len(&mut bytes, signals.len());
118 for signal in signals {
119 encode_text(&mut bytes, &signal.name);
120 encode_json(&mut bytes, None, &signal.input_schema);
121 }
122
123 let mut additional = self.additional_workflows.iter().collect::<Vec<_>>();
124 additional.sort_by(|left, right| left.workflow_type.cmp(&right.workflow_type));
125 encode_len(&mut bytes, additional.len());
126 for workflow in additional {
127 encode_text(&mut bytes, &workflow.workflow_type);
128 encode_json(&mut bytes, None, &workflow.input_schema);
129 encode_json(&mut bytes, None, &workflow.output_schema);
130 }
131
132 let mut unscoped = self.unscoped_activities.iter().collect::<Vec<_>>();
133 unscoped.sort();
134 encode_len(&mut bytes, unscoped.len());
135 for activity in unscoped {
136 encode_text(&mut bytes, activity);
137 }
138 bytes
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
144pub struct WorkerContract {
145 pub task_queue: String,
147 pub actions: Vec<ActionContract>,
149}
150
151impl WorkerContract {
152 fn canonical_bytes(&self) -> Vec<u8> {
153 let mut bytes = Vec::new();
154 encode_text(&mut bytes, &self.task_queue);
155 let mut actions = self
156 .actions
157 .iter()
158 .map(ActionContract::canonical_bytes)
159 .collect::<Vec<_>>();
160 actions.sort();
161 encode_len(&mut bytes, actions.len());
162 for action in actions {
163 update_record(&mut bytes, &action);
164 }
165 bytes
166 }
167}
168
169#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
171pub struct ActivityDescriptor {
172 pub name: String,
174 #[serde(serialize_with = "crate::canonical::serialize_value")]
176 pub input_schema: Value,
177 #[serde(serialize_with = "crate::canonical::serialize_value")]
179 pub output_schema: Value,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub struct ActionContract {
185 pub name: String,
187 #[serde(serialize_with = "crate::canonical::serialize_value")]
189 pub input_schema: Value,
190 #[serde(serialize_with = "crate::canonical::serialize_value")]
192 pub output_schema: Value,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub node: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub timeout: Option<Duration>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub retry: Option<RetryContract>,
202 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
207 pub advisory: bool,
208 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
219 pub agent: bool,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub body: Option<ActionBodyContract>,
233}
234
235#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(tag = "kind", rename_all = "snake_case")]
238pub enum ActionBodyContract {
239 Run {
244 command: String,
246 },
247}
248
249impl ActionContract {
250 fn canonical_bytes(&self) -> Vec<u8> {
251 let mut bytes = Vec::new();
252 encode_text(&mut bytes, &self.name);
253 encode_json(&mut bytes, None, &self.input_schema);
254 encode_json(&mut bytes, None, &self.output_schema);
255 encode_optional_text(&mut bytes, self.node.as_deref());
256 encode_optional_duration(&mut bytes, self.timeout);
257 match &self.retry {
258 None => bytes.push(0),
259 Some(RetryContract::Every { count, every }) => {
260 bytes.push(1);
261 bytes.extend_from_slice(&count.to_be_bytes());
262 encode_duration(&mut bytes, *every);
263 }
264 Some(RetryContract::Backoff { count, min, max }) => {
265 bytes.push(2);
266 bytes.extend_from_slice(&count.to_be_bytes());
267 encode_duration(&mut bytes, *min);
268 encode_duration(&mut bytes, *max);
269 }
270 }
271 if self.advisory {
277 bytes.push(ADVISORY_MARKER);
278 }
279 match &self.body {
292 None => bytes.push(0),
293 Some(ActionBodyContract::Run { command }) => {
294 bytes.push(1);
295 encode_text(&mut bytes, command);
296 }
297 }
298 bytes
299 }
300}
301
302const ADVISORY_MARKER: u8 = 0xA0;
307
308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(tag = "kind", rename_all = "snake_case")]
311pub enum RetryContract {
312 Every {
314 count: u64,
316 every: Duration,
318 },
319 Backoff {
321 count: u64,
323 min: Duration,
325 max: Duration,
327 },
328}
329
330#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
332pub struct ChildContract {
333 pub name: String,
335 #[serde(serialize_with = "crate::canonical::serialize_value")]
337 pub input_schema: Value,
338 #[serde(serialize_with = "crate::canonical::serialize_value")]
340 pub output_schema: Value,
341}
342
343impl ChildContract {
344 fn canonical_bytes(&self) -> Vec<u8> {
345 let mut bytes = Vec::new();
346 encode_text(&mut bytes, &self.name);
347 encode_json(&mut bytes, None, &self.input_schema);
348 encode_json(&mut bytes, None, &self.output_schema);
349 bytes
350 }
351}
352
353#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
355pub struct SignalContract {
356 pub name: String,
358 #[serde(serialize_with = "crate::canonical::serialize_value")]
360 pub input_schema: Value,
361}
362
363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
365pub struct AdditionalWorkflowContract {
366 pub workflow_type: String,
368 #[serde(serialize_with = "crate::canonical::serialize_value")]
370 pub input_schema: Value,
371 #[serde(serialize_with = "crate::canonical::serialize_value")]
373 pub output_schema: Value,
374}
375
376fn encode_json(bytes: &mut Vec<u8>, parent_key: Option<&str>, value: &Value) {
377 match value {
378 Value::Null => bytes.push(0),
379 Value::Bool(value) => bytes.extend_from_slice(&[1, u8::from(*value)]),
380 Value::Number(value) => {
381 bytes.push(2);
382 encode_text(bytes, &value.to_string());
383 }
384 Value::String(value) => {
385 bytes.push(3);
386 encode_text(bytes, value);
387 }
388 Value::Array(values) => {
389 bytes.push(4);
390 let mut values = values.iter().collect::<Vec<_>>();
391 if matches!(parent_key, Some("required" | "enum")) {
392 values.sort_by_key(ToString::to_string);
393 }
394 encode_len(bytes, values.len());
395 for value in values {
396 encode_json(bytes, None, value);
397 }
398 }
399 Value::Object(values) => {
400 bytes.push(5);
401 let mut entries = values.iter().collect::<Vec<_>>();
402 entries.sort_by_key(|(left, _)| *left);
403 encode_len(bytes, entries.len());
404 for (key, value) in entries {
405 encode_text(bytes, key);
406 encode_json(bytes, Some(key), value);
407 }
408 }
409 }
410}
411
412fn encode_len(bytes: &mut Vec<u8>, len: usize) {
413 bytes.extend_from_slice(&(len as u64).to_be_bytes());
414}
415
416fn encode_text(bytes: &mut Vec<u8>, value: &str) {
417 encode_len(bytes, value.len());
418 bytes.extend_from_slice(value.as_bytes());
419}
420
421fn update_record(bytes: &mut Vec<u8>, record: &[u8]) {
422 encode_len(bytes, record.len());
423 bytes.extend_from_slice(record);
424}
425
426fn encode_optional_text(bytes: &mut Vec<u8>, value: Option<&str>) {
427 match value {
428 Some(value) => {
429 bytes.push(1);
430 encode_text(bytes, value);
431 }
432 None => bytes.push(0),
433 }
434}
435
436fn encode_optional_duration(bytes: &mut Vec<u8>, value: Option<Duration>) {
437 match value {
438 Some(value) => {
439 bytes.push(1);
440 encode_duration(bytes, value);
441 }
442 None => bytes.push(0),
443 }
444}
445
446fn encode_duration(bytes: &mut Vec<u8>, value: Duration) {
447 bytes.extend_from_slice(&value.as_secs().to_be_bytes());
448 bytes.extend_from_slice(&value.subsec_nanos().to_be_bytes());
449}
450
451#[cfg(test)]
452#[path = "contract_tests.rs"]
453mod tests;