1use std::fmt;
3
4use tracing::debug;
5
6use crate::{cdr, json, tariff, v211, v221, ParseError, Unversioned, Versioned};
7
8pub type CdrVersion<'buf> = Version<cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
10
11pub type CdrReport<'buf> = Report<'buf, cdr::Versioned<'buf>, cdr::Unversioned<'buf>>;
13
14pub(crate) fn cdr_version(cdr_json: &str) -> Result<CdrVersion<'_>, ParseError> {
18 guess_cdr_version(cdr_json)
19}
20
21pub(crate) fn cdr_version_and_report(cdr_json: &str) -> Result<CdrReport<'_>, ParseError> {
25 let version = guess_cdr_version(cdr_json)?;
26
27 let Version::Certain(cdr) = version else {
28 return Ok(Report {
29 version,
30 unexpected_fields: json::UnexpectedFields::empty(),
31 });
32 };
33
34 let schema = match cdr.version() {
35 crate::Version::V211 => &v211::CDR_SCHEMA,
36 crate::Version::V221 => &v221::CDR_SCHEMA,
37 };
38
39 let report = json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
40 let json::ParseReport {
41 element: _,
42 unexpected_fields,
43 } = report;
44
45 Ok(CdrReport {
46 unexpected_fields,
47 version: Version::Certain(cdr),
48 })
49}
50
51fn guess_cdr_version(source: &str) -> Result<CdrVersion<'_>, ParseError> {
53 const V211_EXCLUSIVE_FIELDS: &[&str] = &["stop_date_time"];
55
56 const V221_EXCLUSIVE_FIELDS: &[&str] = &["end_date_time", "cdr_location", "cdr_token"];
58
59 let element = json::parse(source).map_err(ParseError::from_cdr_err)?;
60 let value = element.value();
61 let json::Value::Object(fields) = value else {
62 return Err(ParseError::cdr_should_be_object());
63 };
64
65 for field in fields {
66 let key = field.key().as_raw();
67
68 if V211_EXCLUSIVE_FIELDS.contains(&key) {
69 return Ok(Version::Certain(cdr::Versioned::new(
70 source,
71 element,
72 crate::Version::V211,
73 )));
74 } else if V221_EXCLUSIVE_FIELDS.contains(&key) {
75 return Ok(Version::Certain(cdr::Versioned::new(
76 source,
77 element,
78 crate::Version::V221,
79 )));
80 }
81 }
82
83 Ok(Version::Uncertain(cdr::Unversioned::new(source, element)))
84}
85
86pub type TariffVersion<'buf> = Version<tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
88
89pub type TariffReport<'buf> = Report<'buf, tariff::Versioned<'buf>, tariff::Unversioned<'buf>>;
91
92pub(super) fn tariff_version(tariff_json: &str) -> Result<TariffVersion<'_>, ParseError> {
96 guess_tariff_version(tariff_json)
97}
98
99pub(super) fn tariff_version_with_report(
103 tariff_json: &str,
104) -> Result<TariffReport<'_>, ParseError> {
105 let version = guess_tariff_version(tariff_json)?;
106
107 let Version::Certain(object) = version else {
108 return Ok(Report {
109 version,
110 unexpected_fields: json::UnexpectedFields::empty(),
111 });
112 };
113
114 let schema = match object.version() {
115 crate::Version::V211 => &v211::TARIFF_SCHEMA,
116 crate::Version::V221 => &v221::TARIFF_SCHEMA,
117 };
118
119 let report =
120 json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_tariff_err)?;
121 let json::ParseReport {
122 element: _,
123 unexpected_fields,
124 } = report;
125
126 Ok(TariffReport {
127 unexpected_fields,
128 version: Version::Certain(object),
129 })
130}
131
132fn guess_tariff_version(source: &str) -> Result<TariffVersion<'_>, ParseError> {
134 const V221_EXCLUSIVE_FIELDS: &[&str] = &[
136 "country_code",
137 "party_id",
138 "type",
139 "min_price",
140 "max_price",
141 "start_date_time",
142 "end_date_time",
143 ];
144
145 const V211_V221_SHARED_FIELDS: &[&str] = &[
147 "id",
148 "currency",
149 "tariff_alt_text",
150 "tariff_alt_url",
151 "elements",
152 "energy_mix",
153 "last_updated",
154 ];
155
156 let element = json::parse(source).map_err(ParseError::from_tariff_err)?;
158 let value = element.value();
159 let json::Value::Object(fields) = value else {
160 return Err(ParseError::tariff_should_be_object());
161 };
162
163 for field in fields {
164 let key = field.key().as_raw();
165
166 if V221_EXCLUSIVE_FIELDS.contains(&key) {
168 debug!("Tariff is v221 because of field: `{key}`");
169 return Ok(TariffVersion::Certain(tariff::Versioned::new(
170 source,
171 element,
172 crate::Version::V221,
173 )));
174 }
175 }
176
177 for field in fields {
178 let key = field.key().as_raw();
179
180 if V211_V221_SHARED_FIELDS.contains(&key) {
181 return Ok(TariffVersion::Certain(tariff::Versioned::new(
182 source,
183 element,
184 crate::Version::V211,
185 )));
186 }
187 }
188
189 Ok(TariffVersion::Uncertain(tariff::Unversioned::new(
190 source, element,
191 )))
192}
193
194#[derive(Debug)]
196pub enum Version<V, U>
197where
198 V: Versioned,
199 U: fmt::Debug,
200{
201 Certain(V),
203
204 Uncertain(U),
206}
207
208impl<V, U> Version<V, U>
209where
210 V: Versioned,
211 U: Unversioned<Versioned = V>,
212{
213 pub fn into_version(self) -> Version<crate::Version, ()> {
216 match self {
217 Version::Certain(v) => Version::Certain(v.version()),
218 Version::Uncertain(_) => Version::Uncertain(()),
219 }
220 }
221
222 pub fn as_version(&self) -> Version<crate::Version, ()> {
225 match self {
226 Version::Certain(v) => Version::Certain(v.version()),
227 Version::Uncertain(_) => Version::Uncertain(()),
228 }
229 }
230
231 pub fn certain_or(self, fallback: crate::Version) -> V {
234 match self {
235 Version::Certain(v) => v,
236 Version::Uncertain(u) => u.force_into_versioned(fallback),
237 }
238 }
239
240 pub fn certain_or_none(self) -> Option<V> {
242 match self {
243 Version::Certain(v) => Some(v),
244 Version::Uncertain(_) => None,
245 }
246 }
247}
248
249#[derive(Debug)]
256pub struct Report<'buf, V, U>
257where
258 V: Versioned,
259 U: fmt::Debug,
260{
261 pub unexpected_fields: json::UnexpectedFields<'buf>,
265
266 pub version: Version<V, U>,
270}
271
272#[cfg(test)]
273mod test {
274 use super::{Unversioned, Version, Versioned};
275
276 impl<V, U> Version<V, U>
277 where
278 V: Versioned,
279 U: Unversioned<Versioned = V>,
280 {
281 pub fn unwrap_certain(self) -> V {
287 match self {
288 Version::Certain(v) => v,
289 Version::Uncertain(_) => panic!("The version of the object is unknown"),
290 }
291 }
292 }
293}
294
295#[cfg(test)]
296mod test_guess_cdr {
297 use assert_matches::assert_matches;
298
299 use crate::{test, Versioned as _};
300
301 use super::{cdr_version_and_report, Report, Version};
302
303 #[test]
304 fn should_guess_cdr_version_v211() {
305 const JSON: &str = include_str!("../test_data/v211/lint/every_field_set/cdr.json");
306
307 test::setup();
308
309 let Report {
310 version,
311 unexpected_fields,
312 } = cdr_version_and_report(JSON).unwrap();
313
314 let cdr = assert_matches!(version, Version::Certain ( cdr ) => cdr );
315 assert_matches!(cdr.version(), crate::Version::V211);
316
317 test::assert_no_unexpected_fields(&unexpected_fields);
318 }
319
320 #[test]
321 fn should_guess_cdr_version_v221() {
322 const JSON: &str = include_str!("../test_data/v221/lint/every_field_set/cdr.json");
323
324 test::setup();
325
326 let Report {
327 version,
328 unexpected_fields,
329 } = cdr_version_and_report(JSON).unwrap();
330
331 let cdr = assert_matches!(version, Version::Certain ( cdr ) => cdr );
332 assert_matches!(cdr.version(), crate::Version::V221);
333
334 test::assert_no_unexpected_fields(&unexpected_fields);
335 }
336}
337
338#[cfg(test)]
339mod test_guess_tariff {
340 use assert_matches::assert_matches;
341
342 use crate::{test, Versioned as _};
343
344 use super::{tariff_version_with_report, Report, Version};
345
346 #[test]
347 fn should_guess_tariff_version_v211() {
348 const JSON: &str = include_str!("../test_data/v211/lint/every_field_set/tariff.json");
349
350 test::setup();
351
352 let Report {
353 version,
354 unexpected_fields,
355 } = tariff_version_with_report(JSON).unwrap();
356
357 let tariff = assert_matches!(version, Version::Certain ( tariff ) => tariff );
358 assert_matches!(tariff.version(), crate::Version::V211);
359
360 test::assert_no_unexpected_fields(&unexpected_fields);
361 }
362
363 #[test]
364 fn should_guess_tariff_version_v221() {
365 const JSON: &str = include_str!("../test_data/v221/lint/every_field_set/tariff.json");
366
367 test::setup();
368
369 let Report {
370 version,
371 unexpected_fields,
372 } = tariff_version_with_report(JSON).unwrap();
373
374 let tariff = assert_matches!(version, Version::Certain ( tariff ) => tariff );
375 assert_matches!(tariff.version(), crate::Version::V221);
376
377 test::assert_no_unexpected_fields(&unexpected_fields);
378 }
379}
380
381#[cfg(test)]
382mod test_real_world {
383 use std::path::Path;
384
385 use assert_matches::assert_matches;
386
387 use crate::{test, Versioned as _};
388
389 use super::{cdr_version_and_report, tariff_version_with_report, Report, Version};
390
391 #[test_each::file(
392 glob = "ocpi-tariffs/test_data/v221/real_world/*/cdr*.json",
393 name(segments = 2)
394 )]
395 fn should_guess_version_v221(cdr_json: &str, path: &Path) {
396 test::setup();
397
398 {
399 let Report {
400 version,
401 unexpected_fields,
402 } = cdr_version_and_report(cdr_json).unwrap();
403
404 let tariff = assert_matches!(version, Version::Certain ( tariff ) => tariff );
405 assert_matches!(tariff.version(), crate::Version::V221);
406
407 test::assert_no_unexpected_fields(&unexpected_fields);
408 }
409
410 {
411 let tariff = std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).ok();
412
413 if let Some(tariff) = tariff {
414 let Report {
415 version: guess,
416 unexpected_fields,
417 } = tariff_version_with_report(&tariff).unwrap();
418
419 let tariff = assert_matches!(guess, Version::Certain ( tariff ) => tariff);
420 assert_matches!(tariff.version(), crate::Version::V221);
421
422 test::assert_no_unexpected_fields(&unexpected_fields);
423 }
424 }
425 }
426
427 #[test_each::file(
428 glob = "ocpi-tariffs/test_data/v211/lint/*/cdr*.json",
429 name(segments = 2)
430 )]
431 fn should_guess_version_v211(cdr_json: &str, path: &Path) {
432 test::setup();
433
434 {
435 let Report {
436 version: guess,
437 unexpected_fields,
438 } = cdr_version_and_report(cdr_json).unwrap();
439
440 let cdr = assert_matches!(guess, Version::Certain ( cdr ) => cdr);
441 assert_matches!(cdr.version(), crate::Version::V211);
442
443 test::assert_no_unexpected_fields(&unexpected_fields);
444 }
445
446 {
447 let tariff_json =
448 std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).ok();
449
450 if let Some(tariff) = tariff_json {
451 let Report {
452 version: guess,
453 unexpected_fields,
454 } = tariff_version_with_report(&tariff).unwrap();
455
456 let tariff = assert_matches!(guess, Version::Certain ( tariff ) => tariff);
457 assert_matches!(tariff.version(), crate::Version::V211);
458
459 test::assert_no_unexpected_fields(&unexpected_fields);
460 }
461 }
462 }
463}