1use std::collections::BTreeMap;
2
3use serde::de::DeserializeOwned;
4use serde_json::Value;
5
6use crate::{
7 Error, Result, ScalarCoercion,
8 binding::{coerce_json_value, deserialize_json_value, nested_value_from_string_map},
9 properties,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum DocumentFormat {
15 Json,
17 Yaml,
19 Toml,
21 Properties,
23 Text,
25 Binary,
27}
28
29impl DocumentFormat {
30 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Json => "JSON",
34 Self::Yaml => "YAML",
35 Self::Toml => "TOML",
36 Self::Properties => "Java properties",
37 Self::Text => "text",
38 Self::Binary => "binary",
39 }
40 }
41
42 pub(crate) fn from_path(path: &str) -> Option<Self> {
43 let (_, extension) = path.rsplit_once('.')?;
44 match extension.to_ascii_lowercase().as_str() {
45 "json" => Some(Self::Json),
46 "yaml" | "yml" => Some(Self::Yaml),
47 "toml" => Some(Self::Toml),
48 "properties" | "props" => Some(Self::Properties),
49 _ => None,
50 }
51 }
52
53 pub(crate) fn from_content_type(content_type: &str) -> Option<Self> {
54 let content_type = content_type.to_ascii_lowercase();
55 if content_type.contains("json") {
56 Some(Self::Json)
57 } else if content_type.contains("yaml") || content_type.contains("yml") {
58 Some(Self::Yaml)
59 } else if content_type.contains("toml") {
60 Some(Self::Toml)
61 } else if content_type.contains("properties") {
62 Some(Self::Properties)
63 } else if content_type.contains("octet-stream") {
64 Some(Self::Binary)
65 } else if content_type.starts_with("text/") {
66 Some(Self::Text)
67 } else {
68 None
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq)]
75pub enum ConfigDocument {
76 Json(Value),
78 Yaml(Value),
80 Toml(Value),
82 Properties(PropertiesDocument),
84 Text(String),
86 Binary(Vec<u8>),
88}
89
90impl ConfigDocument {
91 pub fn format(&self) -> DocumentFormat {
93 match self {
94 Self::Json(_) => DocumentFormat::Json,
95 Self::Yaml(_) => DocumentFormat::Yaml,
96 Self::Toml(_) => DocumentFormat::Toml,
97 Self::Properties(_) => DocumentFormat::Properties,
98 Self::Text(_) => DocumentFormat::Text,
99 Self::Binary(_) => DocumentFormat::Binary,
100 }
101 }
102
103 pub fn to_value(&self) -> Result<Value> {
105 self.to_value_with_coercion(ScalarCoercion::None)
106 }
107
108 pub fn to_value_with_coercion(&self, coercion: ScalarCoercion) -> Result<Value> {
110 match self {
111 Self::Json(value) | Self::Yaml(value) | Self::Toml(value) => {
112 Ok(coerce_json_value(value.clone(), coercion))
113 }
114 Self::Properties(document) => Ok(document.to_value_with_coercion(coercion)),
115 Self::Text(_) => Err(Error::UnsupportedBindingFormat { format: "text" }),
116 Self::Binary(_) => Err(Error::UnsupportedBindingFormat { format: "binary" }),
117 }
118 }
119
120 pub fn deserialize<T>(&self) -> Result<T>
122 where
123 T: DeserializeOwned,
124 {
125 self.deserialize_with_coercion(ScalarCoercion::Smart)
126 }
127
128 pub fn deserialize_with_coercion<T>(&self, coercion: ScalarCoercion) -> Result<T>
130 where
131 T: DeserializeOwned,
132 {
133 deserialize_json_value(
134 self.to_value_with_coercion(coercion)?,
135 format!("{} document", self.format().as_str()),
136 )
137 }
138
139 pub fn deserialize_strict<T>(&self) -> Result<T>
141 where
142 T: DeserializeOwned,
143 {
144 self.deserialize_with_coercion(ScalarCoercion::None)
145 }
146
147 pub(crate) fn from_text(origin: &str, format: DocumentFormat, text: String) -> Result<Self> {
148 match format {
149 DocumentFormat::Json => serde_json::from_str::<Value>(&text)
150 .map(Self::Json)
151 .map_err(|source| Error::Json {
152 url: origin.to_string(),
153 source,
154 }),
155 DocumentFormat::Yaml => serde_yaml::from_str::<Value>(&text)
156 .map(Self::Yaml)
157 .map_err(|source| Error::Yaml {
158 url: origin.to_string(),
159 source,
160 }),
161 DocumentFormat::Toml => {
162 let value = toml::from_str::<toml::Value>(&text).map_err(|source| Error::Toml {
163 url: origin.to_string(),
164 source,
165 })?;
166
167 Ok(Self::Toml(
168 serde_json::to_value(value).expect("serializing TOML value should succeed"),
169 ))
170 }
171 DocumentFormat::Properties => {
172 Ok(Self::Properties(PropertiesDocument::parse(origin, &text)?))
173 }
174 DocumentFormat::Text => Ok(Self::Text(text)),
175 DocumentFormat::Binary => Ok(Self::Binary(text.into_bytes())),
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq)]
182pub struct PropertiesDocument {
183 entries: BTreeMap<String, String>,
184}
185
186impl PropertiesDocument {
187 pub fn parse(origin: &str, text: &str) -> Result<Self> {
189 Ok(Self {
190 entries: properties::parse(text, origin)?,
191 })
192 }
193
194 pub fn entries(&self) -> &BTreeMap<String, String> {
196 &self.entries
197 }
198
199 pub fn into_entries(self) -> BTreeMap<String, String> {
201 self.entries
202 }
203
204 pub fn to_value(&self) -> Value {
206 self.to_value_with_coercion(ScalarCoercion::None)
207 }
208
209 pub fn to_value_with_coercion(&self, coercion: ScalarCoercion) -> Value {
211 nested_value_from_string_map(
212 self.entries
213 .iter()
214 .map(|(key, value)| (key.clone(), value.clone())),
215 coercion,
216 )
217 }
218
219 pub fn deserialize<T>(&self) -> Result<T>
221 where
222 T: DeserializeOwned,
223 {
224 self.deserialize_with_coercion(ScalarCoercion::Smart)
225 }
226
227 pub fn deserialize_with_coercion<T>(&self, coercion: ScalarCoercion) -> Result<T>
229 where
230 T: DeserializeOwned,
231 {
232 deserialize_json_value(self.to_value_with_coercion(coercion), "properties document")
233 }
234
235 pub fn deserialize_strict<T>(&self) -> Result<T>
237 where
238 T: DeserializeOwned,
239 {
240 self.deserialize_with_coercion(ScalarCoercion::None)
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
246pub struct ConfigResource {
247 path: String,
248 url: String,
249 content_type: Option<String>,
250 bytes: Vec<u8>,
251}
252
253impl ConfigResource {
254 pub(crate) fn new(
255 path: String,
256 url: String,
257 content_type: Option<String>,
258 bytes: Vec<u8>,
259 ) -> Self {
260 Self {
261 path,
262 url,
263 content_type,
264 bytes,
265 }
266 }
267
268 pub fn path(&self) -> &str {
270 &self.path
271 }
272
273 pub fn url(&self) -> &str {
275 &self.url
276 }
277
278 pub fn content_type(&self) -> Option<&str> {
280 self.content_type.as_deref()
281 }
282
283 pub fn bytes(&self) -> &[u8] {
285 &self.bytes
286 }
287
288 pub fn into_bytes(self) -> Vec<u8> {
290 self.bytes
291 }
292
293 pub fn text(&self) -> Result<String> {
295 String::from_utf8(self.bytes.clone()).map_err(|source| Error::Utf8 {
296 url: self.url.clone(),
297 source,
298 })
299 }
300
301 pub fn format(&self) -> DocumentFormat {
303 detect_format(&self.path, self.content_type(), &self.bytes)
304 }
305
306 pub fn parse(&self) -> Result<ConfigDocument> {
308 let format = self.format();
309 match format {
310 DocumentFormat::Binary => Ok(ConfigDocument::Binary(self.bytes.clone())),
311 other => ConfigDocument::from_text(&self.url, other, self.text()?),
312 }
313 }
314
315 pub fn deserialize<T>(&self) -> Result<T>
317 where
318 T: DeserializeOwned,
319 {
320 self.parse()?.deserialize()
321 }
322}
323
324fn detect_format(path: &str, content_type: Option<&str>, bytes: &[u8]) -> DocumentFormat {
325 if let Some(format) = DocumentFormat::from_path(path) {
326 return format;
327 }
328
329 if let Some(content_type) = content_type {
330 if let Some(format) = DocumentFormat::from_content_type(content_type) {
331 if format != DocumentFormat::Binary || String::from_utf8(bytes.to_vec()).is_err() {
332 return format;
333 }
334 }
335 }
336
337 if String::from_utf8(bytes.to_vec()).is_ok() {
338 DocumentFormat::Text
339 } else {
340 DocumentFormat::Binary
341 }
342}