1#[cfg(test)]
4mod test;
5
6#[cfg(test)]
7mod test_guess_cdr;
8
9#[cfg(test)]
10mod test_guess_tariff;
11
12#[cfg(test)]
13mod test_real_world;
14
15use std::fmt;
16
17use tracing::debug;
18
19use crate::{cdr, json, tariff, v211, v221, ParseError, ReasonableStr, Unversioned, Versioned};
20
21pub type CdrVersion<'buf> = Version<cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
23
24pub type CdrReport<'buf> = Report<'buf, cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
26
27pub(crate) fn cdr_version(cdr_json: ReasonableStr<'_>) -> Result<CdrVersion<'_>, ParseError> {
31 guess_cdr_version(cdr_json)
32}
33
34pub(crate) fn cdr_version_and_report(
38 cdr_json: ReasonableStr<'_>,
39) -> Result<CdrReport<'_>, ParseError> {
40 let version = guess_cdr_version(cdr_json)?;
41
42 let Version::Certain(cdr) = version else {
43 return Ok(Report {
44 version,
45 unexpected_fields: json::UnexpectedFields::empty(),
46 });
47 };
48
49 let schema = match cdr.version() {
50 crate::Version::V211 => &v211::CDR_SCHEMA,
51 crate::Version::V221 => &v221::CDR_SCHEMA,
52 };
53
54 let report = json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
55 let json::ParseReport {
56 element: _,
57 unexpected_fields,
58 } = report;
59
60 Ok(CdrReport {
61 unexpected_fields,
62 version: Version::Certain(cdr),
63 })
64}
65
66fn guess_cdr_version(source: ReasonableStr<'_>) -> Result<CdrVersion<'_>, ParseError> {
68 const V211_EXCLUSIVE_FIELDS: &[&str] = &["stop_date_time"];
70
71 const V221_EXCLUSIVE_FIELDS: &[&str] = &["end_date_time", "cdr_location", "cdr_token"];
73
74 let element = json::parse(source).map_err(ParseError::from_cdr_err)?;
75 let value = element.value();
76 let json::Value::Object(fields) = value else {
77 return Err(ParseError::cdr_should_be_object());
78 };
79
80 let source = source.into_inner();
81
82 for field in fields {
83 let key = field.key().as_raw();
84
85 if V211_EXCLUSIVE_FIELDS.contains(&key) {
86 return Ok(Version::Certain(cdr::Versioned::new(
87 source,
88 element,
89 crate::Version::V211,
90 )));
91 } else if V221_EXCLUSIVE_FIELDS.contains(&key) {
92 return Ok(Version::Certain(cdr::Versioned::new(
93 source,
94 element,
95 crate::Version::V221,
96 )));
97 }
98 }
99
100 Ok(Version::Uncertain(cdr::Unversioned::new(source, element)))
101}
102
103pub type TariffVersion<'buf> = Version<tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
105
106pub type TariffReport<'buf> = Report<'buf, tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
108
109pub(super) fn tariff_version(
113 tariff_json: ReasonableStr<'_>,
114) -> Result<TariffVersion<'_>, ParseError> {
115 guess_tariff_version(tariff_json)
116}
117
118pub(super) fn tariff_version_with_report(
122 tariff_json: ReasonableStr<'_>,
123) -> Result<TariffReport<'_>, ParseError> {
124 let version = guess_tariff_version(tariff_json)?;
125
126 let Version::Certain(object) = version else {
127 return Ok(Report {
128 version,
129 unexpected_fields: json::UnexpectedFields::empty(),
130 });
131 };
132
133 let schema = match object.version() {
134 crate::Version::V211 => &v211::TARIFF_SCHEMA,
135 crate::Version::V221 => &v221::TARIFF_SCHEMA,
136 };
137
138 let report =
139 json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_tariff_err)?;
140 let json::ParseReport {
141 element: _,
142 unexpected_fields,
143 } = report;
144
145 Ok(TariffReport {
146 unexpected_fields,
147 version: Version::Certain(object),
148 })
149}
150
151fn guess_tariff_version(source: ReasonableStr<'_>) -> Result<TariffVersion<'_>, ParseError> {
153 const V221_EXCLUSIVE_FIELDS: &[&str] = &[
155 "country_code",
156 "party_id",
157 "type",
158 "min_price",
159 "max_price",
160 "start_date_time",
161 "end_date_time",
162 ];
163
164 const V211_V221_SHARED_FIELDS: &[&str] = &[
166 "id",
167 "currency",
168 "tariff_alt_text",
169 "tariff_alt_url",
170 "elements",
171 "energy_mix",
172 "last_updated",
173 ];
174
175 let element = json::parse(source).map_err(ParseError::from_tariff_err)?;
177 let value = element.value();
178 let json::Value::Object(fields) = value else {
179 return Err(ParseError::tariff_should_be_object());
180 };
181 let source = source.into_inner();
182
183 for field in fields {
184 let key = field.key().as_raw();
185
186 if V221_EXCLUSIVE_FIELDS.contains(&key) {
188 debug!("Tariff is v221 because of field: `{key}`");
189 return Ok(TariffVersion::Certain(tariff::Versioned::new(
190 source,
191 element,
192 crate::Version::V221,
193 )));
194 }
195 }
196
197 for field in fields {
198 let key = field.key().as_raw();
199
200 if V211_V221_SHARED_FIELDS.contains(&key) {
201 return Ok(TariffVersion::Certain(tariff::Versioned::new(
202 source,
203 element,
204 crate::Version::V211,
205 )));
206 }
207 }
208
209 Ok(TariffVersion::Uncertain(tariff::Unversioned::new(
210 source, element,
211 )))
212}
213
214#[derive(Debug)]
216pub enum Version<V, U>
217where
218 V: Versioned,
219 U: fmt::Debug,
220{
221 Certain(V),
223
224 Uncertain(U),
226}
227
228impl<V, U> Version<V, U>
229where
230 V: Versioned,
231 U: Unversioned<Versioned = V>,
232{
233 pub fn into_version(self) -> Version<crate::Version, ()> {
236 match self {
237 Version::Certain(v) => Version::Certain(v.version()),
238 Version::Uncertain(_) => Version::Uncertain(()),
239 }
240 }
241
242 pub fn as_version(&self) -> Version<crate::Version, ()> {
245 match self {
246 Version::Certain(v) => Version::Certain(v.version()),
247 Version::Uncertain(_) => Version::Uncertain(()),
248 }
249 }
250
251 pub fn certain_or(self, fallback: crate::Version) -> V {
254 match self {
255 Version::Certain(v) => v,
256 Version::Uncertain(u) => u.force_into_versioned(fallback),
257 }
258 }
259
260 pub fn certain_or_none(self) -> Option<V> {
262 match self {
263 Version::Certain(v) => Some(v),
264 Version::Uncertain(_) => None,
265 }
266 }
267}
268
269#[derive(Debug)]
276pub struct Report<'buf, V, U>
277where
278 V: Versioned,
279 U: fmt::Debug,
280{
281 pub unexpected_fields: json::UnexpectedFields<'buf>,
285
286 pub version: Version<V, U>,
290}