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, 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: &str) -> Result<CdrVersion<'_>, ParseError> {
30 guess_cdr_version(cdr_json)
31}
32
33pub(crate) fn cdr_version_and_report(cdr_json: &str) -> Result<CdrReport<'_>, ParseError> {
37 let version = guess_cdr_version(cdr_json)?;
38
39 let Version::Certain(cdr) = version else {
40 return Ok(Report {
41 version,
42 unexpected_fields: json::UnexpectedFields::empty(),
43 });
44 };
45
46 let schema = match cdr.version() {
47 crate::Version::V211 => &v211::CDR_SCHEMA,
48 crate::Version::V221 => &v221::CDR_SCHEMA,
49 };
50
51 let report = json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
52 let json::ParseReport {
53 element: _,
54 unexpected_fields,
55 } = report;
56
57 Ok(CdrReport {
58 unexpected_fields,
59 version: Version::Certain(cdr),
60 })
61}
62
63fn guess_cdr_version(source: &str) -> Result<CdrVersion<'_>, ParseError> {
65 const V211_EXCLUSIVE_FIELDS: &[&str] = &["stop_date_time"];
67
68 const V221_EXCLUSIVE_FIELDS: &[&str] = &["end_date_time", "cdr_location", "cdr_token"];
70
71 let element = json::parse(source).map_err(ParseError::from_cdr_err)?;
72 let value = element.value();
73 let json::Value::Object(fields) = value else {
74 return Err(ParseError::cdr_should_be_object());
75 };
76
77 for field in fields {
78 let key = field.key().as_raw();
79
80 if V211_EXCLUSIVE_FIELDS.contains(&key) {
81 return Ok(Version::Certain(cdr::Versioned::new(
82 source,
83 element,
84 crate::Version::V211,
85 )));
86 } else if V221_EXCLUSIVE_FIELDS.contains(&key) {
87 return Ok(Version::Certain(cdr::Versioned::new(
88 source,
89 element,
90 crate::Version::V221,
91 )));
92 }
93 }
94
95 Ok(Version::Uncertain(cdr::Unversioned::new(source, element)))
96}
97
98pub type TariffVersion<'buf> = Version<tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
100
101pub type TariffReport<'buf> = Report<'buf, tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
103
104pub(super) fn tariff_version(tariff_json: &str) -> Result<TariffVersion<'_>, ParseError> {
108 guess_tariff_version(tariff_json)
109}
110
111pub(super) fn tariff_version_with_report(
115 tariff_json: &str,
116) -> Result<TariffReport<'_>, ParseError> {
117 let version = guess_tariff_version(tariff_json)?;
118
119 let Version::Certain(object) = version else {
120 return Ok(Report {
121 version,
122 unexpected_fields: json::UnexpectedFields::empty(),
123 });
124 };
125
126 let schema = match object.version() {
127 crate::Version::V211 => &v211::TARIFF_SCHEMA,
128 crate::Version::V221 => &v221::TARIFF_SCHEMA,
129 };
130
131 let report =
132 json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_tariff_err)?;
133 let json::ParseReport {
134 element: _,
135 unexpected_fields,
136 } = report;
137
138 Ok(TariffReport {
139 unexpected_fields,
140 version: Version::Certain(object),
141 })
142}
143
144fn guess_tariff_version(source: &str) -> Result<TariffVersion<'_>, ParseError> {
146 const V221_EXCLUSIVE_FIELDS: &[&str] = &[
148 "country_code",
149 "party_id",
150 "type",
151 "min_price",
152 "max_price",
153 "start_date_time",
154 "end_date_time",
155 ];
156
157 const V211_V221_SHARED_FIELDS: &[&str] = &[
159 "id",
160 "currency",
161 "tariff_alt_text",
162 "tariff_alt_url",
163 "elements",
164 "energy_mix",
165 "last_updated",
166 ];
167
168 let element = json::parse(source).map_err(ParseError::from_tariff_err)?;
170 let value = element.value();
171 let json::Value::Object(fields) = value else {
172 return Err(ParseError::tariff_should_be_object());
173 };
174
175 for field in fields {
176 let key = field.key().as_raw();
177
178 if V221_EXCLUSIVE_FIELDS.contains(&key) {
180 debug!("Tariff is v221 because of field: `{key}`");
181 return Ok(TariffVersion::Certain(tariff::Versioned::new(
182 source,
183 element,
184 crate::Version::V221,
185 )));
186 }
187 }
188
189 for field in fields {
190 let key = field.key().as_raw();
191
192 if V211_V221_SHARED_FIELDS.contains(&key) {
193 return Ok(TariffVersion::Certain(tariff::Versioned::new(
194 source,
195 element,
196 crate::Version::V211,
197 )));
198 }
199 }
200
201 Ok(TariffVersion::Uncertain(tariff::Unversioned::new(
202 source, element,
203 )))
204}
205
206#[derive(Debug)]
208pub enum Version<V, U>
209where
210 V: Versioned,
211 U: fmt::Debug,
212{
213 Certain(V),
215
216 Uncertain(U),
218}
219
220impl<V, U> Version<V, U>
221where
222 V: Versioned,
223 U: Unversioned<Versioned = V>,
224{
225 pub fn into_version(self) -> Version<crate::Version, ()> {
228 match self {
229 Version::Certain(v) => Version::Certain(v.version()),
230 Version::Uncertain(_) => Version::Uncertain(()),
231 }
232 }
233
234 pub fn as_version(&self) -> Version<crate::Version, ()> {
237 match self {
238 Version::Certain(v) => Version::Certain(v.version()),
239 Version::Uncertain(_) => Version::Uncertain(()),
240 }
241 }
242
243 pub fn certain_or(self, fallback: crate::Version) -> V {
246 match self {
247 Version::Certain(v) => v,
248 Version::Uncertain(u) => u.force_into_versioned(fallback),
249 }
250 }
251
252 pub fn certain_or_none(self) -> Option<V> {
254 match self {
255 Version::Certain(v) => Some(v),
256 Version::Uncertain(_) => None,
257 }
258 }
259}
260
261#[derive(Debug)]
268pub struct Report<'buf, V, U>
269where
270 V: Versioned,
271 U: fmt::Debug,
272{
273 pub unexpected_fields: json::UnexpectedFields<'buf>,
277
278 pub version: Version<V, U>,
282}