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}