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 #[must_use]
263 pub fn worker_owed(&self) -> bool {
264 self.body.is_none()
265 }
266
267 fn canonical_bytes(&self) -> Vec<u8> {
268 let mut bytes = Vec::new();
269 encode_text(&mut bytes, &self.name);
270 encode_json(&mut bytes, None, &self.input_schema);
271 encode_json(&mut bytes, None, &self.output_schema);
272 encode_optional_text(&mut bytes, self.node.as_deref());
273 encode_optional_duration(&mut bytes, self.timeout);
274 match &self.retry {
275 None => bytes.push(0),
276 Some(RetryContract::Every { count, every }) => {
277 bytes.push(1);
278 bytes.extend_from_slice(&count.to_be_bytes());
279 encode_duration(&mut bytes, *every);
280 }
281 Some(RetryContract::Backoff { count, min, max }) => {
282 bytes.push(2);
283 bytes.extend_from_slice(&count.to_be_bytes());
284 encode_duration(&mut bytes, *min);
285 encode_duration(&mut bytes, *max);
286 }
287 }
288 if self.advisory {
294 bytes.push(ADVISORY_MARKER);
295 }
296 match &self.body {
309 None => bytes.push(0),
310 Some(ActionBodyContract::Run { command }) => {
311 bytes.push(1);
312 encode_text(&mut bytes, command);
313 }
314 }
315 bytes
316 }
317}
318
319const ADVISORY_MARKER: u8 = 0xA0;
324
325#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(tag = "kind", rename_all = "snake_case")]
328pub enum RetryContract {
329 Every {
331 count: u64,
333 every: Duration,
335 },
336 Backoff {
338 count: u64,
340 min: Duration,
342 max: Duration,
344 },
345}
346
347#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
349pub struct ChildContract {
350 pub name: String,
352 #[serde(serialize_with = "crate::canonical::serialize_value")]
354 pub input_schema: Value,
355 #[serde(serialize_with = "crate::canonical::serialize_value")]
357 pub output_schema: Value,
358}
359
360impl ChildContract {
361 fn canonical_bytes(&self) -> Vec<u8> {
362 let mut bytes = Vec::new();
363 encode_text(&mut bytes, &self.name);
364 encode_json(&mut bytes, None, &self.input_schema);
365 encode_json(&mut bytes, None, &self.output_schema);
366 bytes
367 }
368}
369
370#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
372pub struct SignalContract {
373 pub name: String,
375 #[serde(serialize_with = "crate::canonical::serialize_value")]
377 pub input_schema: Value,
378}
379
380#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
382pub struct AdditionalWorkflowContract {
383 pub workflow_type: String,
385 #[serde(serialize_with = "crate::canonical::serialize_value")]
387 pub input_schema: Value,
388 #[serde(serialize_with = "crate::canonical::serialize_value")]
390 pub output_schema: Value,
391}
392
393fn encode_json(bytes: &mut Vec<u8>, parent_key: Option<&str>, value: &Value) {
394 match value {
395 Value::Null => bytes.push(0),
396 Value::Bool(value) => bytes.extend_from_slice(&[1, u8::from(*value)]),
397 Value::Number(value) => {
398 bytes.push(2);
399 encode_text(bytes, &value.to_string());
400 }
401 Value::String(value) => {
402 bytes.push(3);
403 encode_text(bytes, value);
404 }
405 Value::Array(values) => {
406 bytes.push(4);
407 let mut values = values.iter().collect::<Vec<_>>();
408 if matches!(parent_key, Some("required" | "enum")) {
409 values.sort_by_key(ToString::to_string);
410 }
411 encode_len(bytes, values.len());
412 for value in values {
413 encode_json(bytes, None, value);
414 }
415 }
416 Value::Object(values) => {
417 bytes.push(5);
418 let mut entries = values.iter().collect::<Vec<_>>();
419 entries.sort_by_key(|(left, _)| *left);
420 encode_len(bytes, entries.len());
421 for (key, value) in entries {
422 encode_text(bytes, key);
423 encode_json(bytes, Some(key), value);
424 }
425 }
426 }
427}
428
429fn encode_len(bytes: &mut Vec<u8>, len: usize) {
430 bytes.extend_from_slice(&(len as u64).to_be_bytes());
431}
432
433fn encode_text(bytes: &mut Vec<u8>, value: &str) {
434 encode_len(bytes, value.len());
435 bytes.extend_from_slice(value.as_bytes());
436}
437
438fn update_record(bytes: &mut Vec<u8>, record: &[u8]) {
439 encode_len(bytes, record.len());
440 bytes.extend_from_slice(record);
441}
442
443fn encode_optional_text(bytes: &mut Vec<u8>, value: Option<&str>) {
444 match value {
445 Some(value) => {
446 bytes.push(1);
447 encode_text(bytes, value);
448 }
449 None => bytes.push(0),
450 }
451}
452
453fn encode_optional_duration(bytes: &mut Vec<u8>, value: Option<Duration>) {
454 match value {
455 Some(value) => {
456 bytes.push(1);
457 encode_duration(bytes, value);
458 }
459 None => bytes.push(0),
460 }
461}
462
463fn encode_duration(bytes: &mut Vec<u8>, value: Duration) {
464 bytes.extend_from_slice(&value.as_secs().to_be_bytes());
465 bytes.extend_from_slice(&value.subsec_nanos().to_be_bytes());
466}
467
468#[cfg(test)]
469#[path = "contract_tests.rs"]
470mod tests;