1use sim_kernel::{CapabilityName, Cx, Value};
4
5use crate::{Doc, DocKind, OfficeError, fidelity::FidelityReport};
6
7#[derive(Clone, Debug, PartialEq)]
9pub struct DocCodecOptions(pub Value);
10
11impl DocCodecOptions {
12 #[must_use]
14 pub fn new(value: Value) -> Self {
15 Self(value)
16 }
17
18 #[must_use]
20 pub fn value(&self) -> &Value {
21 &self.0
22 }
23}
24
25pub trait DocCodec {
27 fn codec_id(&self) -> &'static str;
29 fn kinds(&self) -> &'static [DocKind];
31 fn decode(
33 &self,
34 cx: &mut Cx,
35 bytes: &[u8],
36 options: &DocCodecOptions,
37 ) -> Result<(Doc, FidelityReport), OfficeError>;
38 fn encode(
40 &self,
41 cx: &mut Cx,
42 doc: &Doc,
43 options: &DocCodecOptions,
44 ) -> Result<(Vec<u8>, FidelityReport), OfficeError>;
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct DocSite {
50 pub site_id: String,
52 pub kinds: Vec<DocKind>,
54 pub required_caps: Vec<CapabilityName>,
56 pub default_modeled: bool,
58}
59
60impl DocSite {
61 #[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 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#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum Placement {
90 LocalStore,
92 Codec(String),
94 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}