Skip to main content

energyml_storage/
write.rs

1//! The writing half of the storage interface, shaped after ETP like the reading
2//! half.
3//!
4//! [`EnergymlWriter`] is to `PutDataObjects`, `DeleteDataObjects`, the DataArray
5//! writes and the Transaction protocol what [`crate::EnergymlStorage`] is to the
6//! reads: plural, partial by nature, addressed by URI. An EPC implements it, so
7//! does an ETP dataspace; a server's handlers are written against it.
8//!
9//! # A writer is one dataspace
10//!
11//! Nothing here names a dataspace: a writer writes into the one it is. A store
12//! holding several hands out one writer per dataspace through
13//! [`crate::dataspaces::EnergymlDataspaces`]. That is also the unit a server
14//! serialises writes on - one transaction at a time per dataspace.
15//!
16//! # A transaction is an object
17//!
18//! [`EnergymlWriter::begin`] returns a [`Transaction`], and committing or
19//! rolling back names it. A transaction is not a mode of the store that any
20//! caller could end: ending one with a [`Transaction`] that is not the open one
21//! is refused by name. ETP allows one transaction per session (ETP 1.2 §18,
22//! `StartTransaction`); a writer allows one at a time.
23//!
24//! **Outside a transaction, each write stands alone**: nothing groups two calls,
25//! and a caller that needs them applied together begins a transaction first.
26//!
27//! **After a refused end**, the two cases differ, after ETP: a refused commit
28//! applied nothing and the transaction is **over** (ETP 1.2 §18.1.1.1, step
29//! 8.b.ii); a refused rollback leaves it **open** (the specification does not say
30//! otherwise; `TODO(verify)` §18.1.1.1, step 6.b). [`EnergymlWriter::open_transaction`]
31//! tells which.
32
33use energyml_core::registry::DynObject;
34
35use crate::array::{ArrayData, ArrayLocation, ArrayMetadata};
36use crate::error::{Error, Result};
37use crate::raw::RawObject;
38use crate::storage::EnergymlStorage;
39
40use async_trait::async_trait;
41
42/// What ETP's `StartTransaction` asks for.
43///
44/// Built with [`TransactionRequest::new`]; `#[non_exhaustive]`.
45#[derive(Debug, Clone, Default, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct TransactionRequest {
48    /// The dataspaces the transaction covers. Empty: the writer's own.
49    pub dataspaces: Vec<String>,
50    /// A transaction that only reads (a consistent view), writing nothing.
51    pub read_only: bool,
52    /// What the transaction is for, as the caller words it.
53    pub message: String,
54}
55
56impl TransactionRequest {
57    /// A writing transaction over the writer's own dataspace.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Only reading.
63    pub fn read_only(mut self) -> Self {
64        self.read_only = true;
65        self
66    }
67
68    /// With a message.
69    pub fn with_message(mut self, message: impl Into<String>) -> Self {
70        self.message = message.into();
71        self
72    }
73
74    /// Over these dataspaces.
75    pub fn over(mut self, dataspaces: impl IntoIterator<Item = impl Into<String>>) -> Self {
76        self.dataspaces = dataspaces.into_iter().map(Into::into).collect();
77        self
78    }
79}
80
81/// An open transaction, as [`EnergymlWriter::begin`] returned it.
82///
83/// `#[non_exhaustive]`: only a writer makes one.
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[non_exhaustive]
86pub struct Transaction {
87    /// The writer's name for it: the server's `transactionUuid` for an ETP
88    /// dataspace, a number the store gives for an EPC.
89    pub id: String,
90    /// What was asked.
91    pub request: TransactionRequest,
92}
93
94impl Transaction {
95    /// A transaction a writer has opened.
96    pub fn new(id: impl Into<String>, request: TransactionRequest) -> Self {
97        Self {
98            id: id.into(),
99            request,
100        }
101    }
102}
103
104/// What writing one object did: ETP's `PutResponse`.
105///
106/// Built with [`Put::new`]; `#[non_exhaustive]`.
107#[derive(Debug, Clone, Default, PartialEq, Eq)]
108#[non_exhaustive]
109pub struct Put {
110    /// The full URI the object is now known under.
111    pub uri: String,
112    /// Contained objects (a WITSML container's parts) the write created.
113    pub created_contained: Vec<String>,
114    /// Contained objects it deleted (`pruneContainedObjects`).
115    pub deleted_contained: Vec<String>,
116    /// Contained objects it joined to this container.
117    pub joined_contained: Vec<String>,
118    /// Contained objects it unjoined from it.
119    pub unjoined_contained: Vec<String>,
120}
121
122impl Put {
123    /// An object written, nothing contained touched.
124    pub fn new(uri: impl Into<String>) -> Self {
125        Self {
126            uri: uri.into(),
127            ..Self::default()
128        }
129    }
130}
131
132/// The writing half of a store: objects, arrays, transactions - ETP protocols
133/// 4, 9 and 18.
134///
135/// What a store cannot do it refuses **by name** ([`Error::Unsupported`]), and
136/// says so in advance in [`EnergymlStorage::capabilities`]; nothing here answers
137/// an empty success for a capability it lacks.
138// `async_trait` marks the boxed future of each default method `#[must_use]`,
139// which clippy 1.99 (`double_must_use`) reads as ours: see `storage.rs`.
140#[allow(clippy::double_must_use)]
141#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
142#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
143pub trait EnergymlWriter: EnergymlStorage {
144    /// ETP `PutDataObjects`: upsert each object (ETP 1.2 §9, `PutDataObjects`:
145    /// a complete replacement, never a merge). One result per object, in order.
146    ///
147    /// `prune_contained`: a container's contained objects that the new version
148    /// no longer contains are deleted. A store that does not know containment
149    /// refuses `true` by name rather than ignoring it.
150    async fn put_objects(
151        &mut self,
152        objects: Vec<Box<dyn DynObject>>,
153        prune_contained: bool,
154    ) -> Vec<Result<Put>>;
155
156    /// The same with the objects as bytes, kept as received where the store can
157    /// ([`crate::raw`]). Refused by default.
158    async fn put_raw(&mut self, objects: Vec<RawObject>, prune_contained: bool) -> Vec<Result<Put>> {
159        let _ = prune_contained;
160        objects
161            .iter()
162            .map(|_| {
163                Err(Error::Unsupported {
164                    operation: "writing objects as bytes",
165                    store: self.store_kind(),
166                })
167            })
168            .collect()
169    }
170
171    /// ETP `DeleteDataObjects`: per URI, the URIs actually deleted (the object,
172    /// and its contained objects when `prune_contained`).
173    async fn delete_objects(&mut self, uris: &[String], prune_contained: bool) -> Vec<Result<Vec<String>>>;
174
175    /// ETP `StartTransaction`. One transaction at a time: a second is refused.
176    async fn begin(&mut self, request: TransactionRequest) -> Result<Transaction>;
177
178    /// ETP `CommitTransaction`, of the transaction [`EnergymlWriter::begin`] returned.
179    async fn commit(&mut self, transaction: &Transaction) -> Result<()>;
180
181    /// ETP `RollbackTransaction`, of the transaction [`EnergymlWriter::begin`] returned.
182    async fn rollback(&mut self, transaction: &Transaction) -> Result<()>;
183
184    /// The transaction open now, if any.
185    fn open_transaction(&self) -> Option<Transaction>;
186
187    /// ETP `PutUninitializedDataArrays`: declare each array's shape and type
188    /// before any value is written, so that it can be written part by part.
189    /// Refused by default.
190    async fn declare_arrays(&mut self, arrays: &[(ArrayLocation, ArrayMetadata)]) -> Vec<Result<()>> {
191        arrays
192            .iter()
193            .map(|_| {
194                Err(Error::Unsupported {
195                    operation: "declaring an array before writing it",
196                    store: self.store_kind(),
197                })
198            })
199            .collect()
200    }
201
202    /// ETP `PutDataArrays` and `PutDataSubarrays`: the whole array when the
203    /// location's `start` and `count` are empty, the block they name otherwise -
204    /// the same addressing [`EnergymlStorage::read_array`] reads.
205    async fn put_arrays(&mut self, arrays: &[(ArrayLocation, ArrayData)]) -> Vec<Result<()>>;
206}
207
208/// Refuse ending a transaction that is not the open one: the shared check of
209/// every writer, so that the wording is one.
210pub fn check_open(open: Option<&Transaction>, given: &Transaction, store: &'static str) -> Result<()> {
211    match open {
212        Some(open) if open.id == given.id => Ok(()),
213        Some(open) => Err(Error::Transaction {
214            store,
215            reason: format!(
216                "transaction `{}` is not the open one (`{}` is)",
217                given.id, open.id
218            ),
219        }),
220        None => Err(Error::Transaction {
221            store,
222            reason: format!("transaction `{}` is not open (no transaction is)", given.id),
223        }),
224    }
225}