Skip to main content

sim_lib_doc_core/
place.rs

1//! Placement contracts for document codecs and service sites.
2
3use sim_kernel::{CapabilityName, Cx, Value};
4
5use crate::{Doc, DocKind, OfficeError, fidelity::FidelityReport};
6
7/// Opaque options passed to document codecs.
8#[derive(Clone, Debug, PartialEq)]
9pub struct DocCodecOptions(pub Value);
10
11impl DocCodecOptions {
12    /// Wraps an opaque runtime value as codec options.
13    #[must_use]
14    pub fn new(value: Value) -> Self {
15        Self(value)
16    }
17
18    /// Borrows the wrapped runtime value.
19    #[must_use]
20    pub fn value(&self) -> &Value {
21        &self.0
22    }
23}
24
25/// File or wire codec for one or more document kinds.
26pub trait DocCodec {
27    /// Stable codec id such as `codec/ooxml-xlsx`.
28    fn codec_id(&self) -> &'static str;
29    /// Document kinds accepted by this codec.
30    fn kinds(&self) -> &'static [DocKind];
31    /// Decodes bytes into a document plus fidelity report.
32    fn decode(
33        &self,
34        cx: &mut Cx,
35        bytes: &[u8],
36        options: &DocCodecOptions,
37    ) -> Result<(Doc, FidelityReport), OfficeError>;
38    /// Encodes a document into bytes plus fidelity report.
39    fn encode(
40        &self,
41        cx: &mut Cx,
42        doc: &Doc,
43        options: &DocCodecOptions,
44    ) -> Result<(Vec<u8>, FidelityReport), OfficeError>;
45}
46
47/// A service or helper-process placement for document operations.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct DocSite {
50    /// Stable site id such as `site/msgraph`.
51    pub site_id: String,
52    /// Document kinds served by this site.
53    pub kinds: Vec<DocKind>,
54    /// Capabilities required for live access.
55    pub required_caps: Vec<CapabilityName>,
56    /// Whether the site defaults to deterministic modeled behavior.
57    pub default_modeled: bool,
58}
59
60impl DocSite {
61    /// Builds a document site.
62    #[must_use]
63    pub fn new(
64        site_id: impl Into<String>,
65        kinds: Vec<DocKind>,
66        required_caps: Vec<CapabilityName>,
67        default_modeled: bool,
68    ) -> Self {
69        Self {
70            site_id: site_id.into(),
71            kinds,
72            required_caps,
73            default_modeled,
74        }
75    }
76
77    /// Requires live capabilities unless this site is currently modeled.
78    pub fn authorize(&self, cx: &Cx) -> Result<(), OfficeError> {
79        if self.default_modeled {
80            return Ok(());
81        }
82        cx.require_all(&self.required_caps)
83            .map_err(OfficeError::from)
84    }
85}
86
87/// Where a document operation is placed.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum Placement {
90    /// The local document store.
91    LocalStore,
92    /// A named file or wire codec.
93    Codec(String),
94    /// A named service or helper-process site.
95    Site(String),
96}
97
98#[cfg(test)]
99mod tests {
100    use std::sync::Arc;
101
102    use sim_kernel::{DefaultFactory, NoopEvalPolicy};
103
104    use crate::{DocId, caps::NET_CONNECT_CAPABILITY};
105
106    use super::*;
107
108    struct EchoCodec;
109
110    impl DocCodec for EchoCodec {
111        fn codec_id(&self) -> &'static str {
112            "codec/echo"
113        }
114
115        fn kinds(&self) -> &'static [DocKind] {
116            static KINDS: std::sync::OnceLock<Vec<DocKind>> = std::sync::OnceLock::new();
117            KINDS.get_or_init(|| vec![DocKind::new("report")])
118        }
119
120        fn decode(
121            &self,
122            _cx: &mut Cx,
123            bytes: &[u8],
124            options: &DocCodecOptions,
125        ) -> Result<(Doc, FidelityReport), OfficeError> {
126            let body = options.value().clone();
127            let doc = Doc::new(
128                DocKind::new(String::from_utf8_lossy(bytes).to_string()),
129                DocId::new("decoded"),
130                body,
131                vec![],
132            );
133            Ok((
134                doc,
135                FidelityReport::new(self.codec_id()).with_warning("used options body"),
136            ))
137        }
138
139        fn encode(
140            &self,
141            _cx: &mut Cx,
142            doc: &Doc,
143            options: &DocCodecOptions,
144        ) -> Result<(Vec<u8>, FidelityReport), OfficeError> {
145            let uses_options = options.value() == &doc.body;
146            let report = if uses_options {
147                FidelityReport::new(self.codec_id()).with_warning("options matched body")
148            } else {
149                FidelityReport::new(self.codec_id())
150            };
151            Ok((doc.kind.as_str().as_bytes().to_vec(), report))
152        }
153    }
154
155    #[test]
156    fn codec_options_select_behavior() {
157        let mut cx = Cx::new(
158            Arc::new(NoopEvalPolicy),
159            Arc::new(DefaultFactory),
160            sim_kernel::HandleSeed::new(0x6032_f288_5915_ed19),
161        );
162        let option_value = cx.factory().string("option-body".to_owned()).unwrap();
163        let options = DocCodecOptions::new(option_value.clone());
164        let codec = EchoCodec;
165
166        let (doc, decode_report) = codec.decode(&mut cx, b"report", &options).unwrap();
167        let (encoded, encode_report) = codec.encode(&mut cx, &doc, &options).unwrap();
168
169        assert_eq!(doc.body, option_value);
170        assert_eq!(encoded, b"report");
171        assert_eq!(decode_report.warnings, vec!["used options body"]);
172        assert_eq!(encode_report.warnings, vec!["options matched body"]);
173    }
174
175    #[test]
176    fn live_site_requiring_network_is_denied_by_default() {
177        let cx = Cx::new(
178            Arc::new(NoopEvalPolicy),
179            Arc::new(DefaultFactory),
180            sim_kernel::HandleSeed::new(0x90d5_087a_2e1d_712b),
181        );
182        let site = DocSite::new(
183            "site/msgraph",
184            vec![DocKind::new("sheet")],
185            vec![CapabilityName::new(NET_CONNECT_CAPABILITY)],
186            false,
187        );
188
189        let denied = site.authorize(&cx).unwrap_err();
190
191        assert!(
192            matches!(denied, OfficeError::CapabilityDenied(capability) if capability.as_str() == NET_CONNECT_CAPABILITY)
193        );
194    }
195
196    #[test]
197    fn modeled_site_is_allowed_without_live_capabilities() {
198        let cx = Cx::new(
199            Arc::new(NoopEvalPolicy),
200            Arc::new(DefaultFactory),
201            sim_kernel::HandleSeed::new(0x4b15_b1ee_0a29_abd7),
202        );
203        let site = DocSite::new(
204            "site/msgraph",
205            vec![DocKind::new("sheet")],
206            vec![CapabilityName::new(NET_CONNECT_CAPABILITY)],
207            true,
208        );
209
210        site.authorize(&cx).unwrap();
211    }
212}