Skip to main content

dex_sdk/flow_definition/
mod.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use std::time::SystemTime;
10
11use dex_protocol::dex::{
12    FlowDefinitionUpload, FlowDefinitionUploadOutcome as ProtoFlowDefinitionUploadOutcome,
13    FlowDefinitionUploadResult as ProtoFlowDefinitionUploadResult, UploadFlowDefinitionsRequest,
14    UploadFlowDefinitionsResponse,
15};
16
17#[cfg(test)]
18mod conformance_tests;
19mod definition_sources;
20mod generated_at;
21mod source_path;
22mod worker_upload;
23#[cfg(test)]
24mod worker_upload_tests;
25
26pub(crate) use definition_sources::{collect_flow_definitions, validate_document};
27pub(crate) use source_path::{current_working_directory, rewrite_source_paths};
28pub(crate) use worker_upload::upload_flow_definitions_at_worker_start;
29
30const MAX_DEFINITIONS_PER_REQUEST: usize = 256;
31// Half the Server's default 32 MiB gRPC message limit; a larger document goes alone.
32const MAX_CONTENT_BYTES_PER_REQUEST: usize = 16 * 1024 * 1024;
33
34/// Holds one Flow Definition Graph JSON document in memory for upload to the Dex Server.
35///
36/// Dex Web draws a Flow type's graph from the document stored for it. Pass documents to
37/// [`crate::WorkerOptions::flow_definitions`] to upload them when a Worker starts, or to
38/// [`crate::Client::upload_flow_definitions`] from a deploy script. Use
39/// [`crate::WorkerOptions::flow_definition_paths`] instead when the documents are files, such as
40/// the output of `dexcli visualize`.
41///
42/// The Flow type must equal `flow.name` in the content. The content must be a UTF-8 JSON object
43/// whose `schemaVersion` starts with `2.`. The Dex Server compares `generated_at` with the stored
44/// document: a different document replaces the stored one only when its generation time is
45/// strictly later. Pass a fixed time for a document generated from a template, such as the
46/// template's release time, so every Worker sends the same time.
47///
48/// `Debug` prints the content length, not the content.
49///
50/// Since 1.45.0.
51///
52/// # Examples
53///
54/// ```no_run
55/// use std::time::{Duration, SystemTime};
56/// use dex_sdk::{FlowDefinitionDocument, WorkerOptions};
57///
58/// let content = std::fs::read("flow-definitions/OrderFlow.json")?;
59/// let generated_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_793_491_200);
60/// let options = WorkerOptions::new().flow_definitions([FlowDefinitionDocument::new(
61///     "OrderFlow",
62///     content,
63///     generated_at,
64/// )]);
65/// # Ok::<(), Box<dyn std::error::Error>>(())
66/// ```
67#[derive(Clone, PartialEq, Eq)]
68pub struct FlowDefinitionDocument {
69    flow_type: String,
70    content: Vec<u8>,
71    generated_at: SystemTime,
72}
73
74impl FlowDefinitionDocument {
75    /// Creates a document from its Flow type, JSON content, and generation time.
76    ///
77    /// `flow_type` must equal `flow.name` in `content`, and `content` must hold the UTF-8 JSON
78    /// bytes of the Flow Definition Graph. The constructor does not validate them.
79    /// [`crate::Worker::try_new`] returns [`crate::SdkError::FlowDefinition`], and
80    /// [`crate::Client::upload_flow_definitions`] returns [`crate::SdkError::InvalidArgument`],
81    /// when a document is not a UTF-8 JSON object whose `schemaVersion` starts with `2.` and whose
82    /// `flow.name` equals the Flow type. The Dex Server rejects other invalid documents.
83    ///
84    /// Since 1.45.0.
85    pub fn new(
86        flow_type: impl Into<String>,
87        content: impl Into<Vec<u8>>,
88        generated_at: SystemTime,
89    ) -> Self {
90        Self {
91            flow_type: flow_type.into(),
92            content: content.into(),
93            generated_at,
94        }
95    }
96
97    /// Returns the Flow type that the document describes.
98    ///
99    /// Since 1.45.0.
100    pub fn flow_type(&self) -> &str {
101        &self.flow_type
102    }
103
104    /// Returns the UTF-8 JSON bytes of the document.
105    ///
106    /// Since 1.45.0.
107    pub fn content(&self) -> &[u8] {
108        &self.content
109    }
110
111    /// Returns the time when the document was generated.
112    ///
113    /// Since 1.45.0.
114    pub fn generated_at(&self) -> SystemTime {
115        self.generated_at
116    }
117}
118
119impl std::fmt::Debug for FlowDefinitionDocument {
120    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        formatter
122            .debug_struct("FlowDefinitionDocument")
123            .field("flow_type", &self.flow_type)
124            .field("content_bytes", &self.content.len())
125            .field("generated_at", &self.generated_at)
126            .finish()
127    }
128}
129
130/// Reports what the Dex Server did with one uploaded Flow definition.
131///
132/// The Server decides each Flow type separately. A stored document is replaced only by a
133/// different document with a strictly later generation time, unless the upload uses `force`.
134/// See [`crate::Client::upload_flow_definitions`] for an example.
135///
136/// Since 1.45.0.
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138#[non_exhaustive]
139pub enum FlowDefinitionUploadOutcome {
140    /// No document was stored for the Flow type; the uploaded one is now stored.
141    Created,
142    /// The uploaded document replaced a different, older one.
143    Replaced,
144    /// The stored document is identical; nothing was written.
145    Unchanged,
146    /// The stored document differs and is at least as new, so it stays.
147    ///
148    /// Upload with `force` to replace it anyway.
149    KeptExisting,
150    /// The Server reported an outcome that this SDK version does not know.
151    Unknown,
152}
153
154/// Describes the outcome of uploading one Flow definition and the document stored afterwards.
155///
156/// [`crate::Client::upload_flow_definitions`] returns one result for each uploaded document, in
157/// request order. The stored digest and generation time describe the document that the Server
158/// holds after the call. They differ from the uploaded document only for
159/// [`FlowDefinitionUploadOutcome::KeptExisting`].
160///
161/// Since 1.45.0.
162#[derive(Clone, Debug, Eq, PartialEq)]
163pub struct FlowDefinitionUploadResult {
164    flow_type: String,
165    outcome: FlowDefinitionUploadOutcome,
166    stored_digest: String,
167    stored_generated_at: Option<SystemTime>,
168}
169
170impl FlowDefinitionUploadResult {
171    /// Returns the Flow type of the uploaded document.
172    ///
173    /// Since 1.45.0.
174    pub fn flow_type(&self) -> &str {
175        &self.flow_type
176    }
177
178    /// Returns what the Server did with the uploaded document.
179    ///
180    /// Since 1.45.0.
181    pub fn outcome(&self) -> FlowDefinitionUploadOutcome {
182        self.outcome
183    }
184
185    /// Returns the lowercase hexadecimal SHA-256 digest of the document stored after the call.
186    ///
187    /// Since 1.45.0.
188    pub fn stored_digest(&self) -> &str {
189        &self.stored_digest
190    }
191
192    /// Returns the generation time of the document stored after the call.
193    ///
194    /// Returns the time at microsecond precision, or `None` when the Server did not report one
195    /// or it is outside the platform's `SystemTime` range.
196    ///
197    /// Since 1.45.0.
198    pub fn stored_generated_at(&self) -> Option<SystemTime> {
199        self.stored_generated_at
200    }
201}
202
203fn uploader() -> String {
204    format!("sdk-rust/{}", crate::server_protocol::SDK_VERSION)
205}
206
207/// Builds upload requests of at most 256 documents each, in document order.
208pub(crate) fn upload_requests(
209    documents: Vec<FlowDefinitionDocument>,
210    force: bool,
211) -> Vec<UploadFlowDefinitionsRequest> {
212    let mut requests: Vec<UploadFlowDefinitionsRequest> = Vec::new();
213    for document in documents {
214        let needs_new_request = requests.last().is_none_or(|request| {
215            request.definitions.len() == MAX_DEFINITIONS_PER_REQUEST
216                || request_content_bytes(request) + document.content.len()
217                    > MAX_CONTENT_BYTES_PER_REQUEST
218        });
219        if needs_new_request {
220            requests.push(UploadFlowDefinitionsRequest {
221                definitions: Vec::new(),
222                uploader: uploader(),
223                force,
224            });
225        }
226        let request = requests
227            .last_mut()
228            .expect("a request was pushed for this document");
229        request.definitions.push(FlowDefinitionUpload {
230            flow_type: document.flow_type,
231            content: document.content,
232            generated_at: Some(generated_at::truncate_timestamp_to_millis(
233                prost_types::Timestamp::from(document.generated_at),
234            )),
235        });
236    }
237    requests
238}
239
240fn request_content_bytes(request: &UploadFlowDefinitionsRequest) -> usize {
241    request
242        .definitions
243        .iter()
244        .map(|definition| definition.content.len())
245        .sum()
246}
247
248pub(crate) fn map_upload_results(
249    response: UploadFlowDefinitionsResponse,
250) -> Vec<FlowDefinitionUploadResult> {
251    response
252        .results
253        .into_iter()
254        .map(map_upload_result)
255        .collect()
256}
257
258pub(crate) fn map_upload_result(
259    result: ProtoFlowDefinitionUploadResult,
260) -> FlowDefinitionUploadResult {
261    let outcome = match ProtoFlowDefinitionUploadOutcome::try_from(result.outcome) {
262        Ok(ProtoFlowDefinitionUploadOutcome::Created) => FlowDefinitionUploadOutcome::Created,
263        Ok(ProtoFlowDefinitionUploadOutcome::Replaced) => FlowDefinitionUploadOutcome::Replaced,
264        Ok(ProtoFlowDefinitionUploadOutcome::Unchanged) => FlowDefinitionUploadOutcome::Unchanged,
265        Ok(ProtoFlowDefinitionUploadOutcome::KeptExisting) => {
266            FlowDefinitionUploadOutcome::KeptExisting
267        }
268        Ok(ProtoFlowDefinitionUploadOutcome::Unspecified) | Err(_) => {
269            FlowDefinitionUploadOutcome::Unknown
270        }
271    };
272    FlowDefinitionUploadResult {
273        flow_type: result.flow_type,
274        outcome,
275        stored_digest: result.stored_digest,
276        // A time outside the SystemTime range is reported as None, as documented.
277        stored_generated_at: result
278            .stored_generated_at
279            .and_then(|timestamp| SystemTime::try_from(timestamp).ok()),
280    }
281}