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