1use serde::{
8 de::{self, Visitor},
9 ser::SerializeStruct,
10 Deserialize, Deserializer, Serialize, Serializer,
11};
12use serde_json::{Map, Value};
13use std::{fmt, num::NonZeroUsize};
14
15pub const SOLVE_JSON_SCHEMA_VERSION_V1: u32 = 1;
17
18pub const MAX_SOLVE_JSON_SAMPLES_V1: usize = 10_000;
23
24fn deserialize_present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
30where
31 D: Deserializer<'de>,
32 T: Deserialize<'de>,
33{
34 T::deserialize(deserializer).map(Some)
35}
36
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
42pub struct SchemaVersionV1;
43
44impl SchemaVersionV1 {
45 pub const fn get(self) -> u32 {
47 SOLVE_JSON_SCHEMA_VERSION_V1
48 }
49}
50
51impl Serialize for SchemaVersionV1 {
52 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53 where
54 S: Serializer,
55 {
56 serializer.serialize_u32(SOLVE_JSON_SCHEMA_VERSION_V1)
57 }
58}
59
60impl<'de> Deserialize<'de> for SchemaVersionV1 {
61 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
62 where
63 D: Deserializer<'de>,
64 {
65 struct SchemaVersionVisitor;
66
67 impl<'de> Visitor<'de> for SchemaVersionVisitor {
68 type Value = SchemaVersionV1;
69
70 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 formatter.write_str("the integer 1")
72 }
73
74 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
75 where
76 E: de::Error,
77 {
78 if value == u64::from(SOLVE_JSON_SCHEMA_VERSION_V1) {
79 Ok(SchemaVersionV1)
80 } else {
81 Err(E::custom(format!(
82 "unsupported schema_version {value}; expected {SOLVE_JSON_SCHEMA_VERSION_V1}"
83 )))
84 }
85 }
86
87 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
88 where
89 E: de::Error,
90 {
91 if value == i64::from(SOLVE_JSON_SCHEMA_VERSION_V1) {
92 Ok(SchemaVersionV1)
93 } else {
94 Err(E::custom(format!(
95 "unsupported schema_version {value}; expected {SOLVE_JSON_SCHEMA_VERSION_V1}"
96 )))
97 }
98 }
99 }
100
101 deserializer.deserialize_any(SchemaVersionVisitor)
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct SolveRequestV1 {
112 pub schema_version: SchemaVersionV1,
114 pub projectile: ProjectileV1,
115 pub rifle: RifleV1,
116 pub shot: ShotV1,
117 pub atmosphere: AtmosphereV1,
118 pub wind: WindV1,
119 pub solver: SolverV1,
120 pub effects: EffectsV1,
121 pub sampling: SamplingV1,
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct ProjectileV1 {
128 pub mass_kg: f64,
129 pub diameter_m: f64,
130 #[serde(
131 default,
132 skip_serializing_if = "Option::is_none",
133 deserialize_with = "deserialize_present"
134 )]
135 pub length_m: Option<f64>,
136 pub drag_model: DragModelV1,
137 pub ballistic_coefficient: f64,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum DragModelV1 {
143 #[serde(rename = "G1")]
144 G1,
145 #[serde(rename = "G6")]
146 G6,
147 #[serde(rename = "G7")]
148 G7,
149 #[serde(rename = "G8")]
150 G8,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(deny_unknown_fields)]
156pub struct RifleV1 {
157 pub muzzle_velocity_mps: f64,
158 #[serde(
159 default,
160 skip_serializing_if = "Option::is_none",
161 deserialize_with = "deserialize_present"
162 )]
163 pub sight_height_m: Option<f64>,
164 #[serde(
165 default,
166 skip_serializing_if = "Option::is_none",
167 deserialize_with = "deserialize_present"
168 )]
169 pub muzzle_height_m: Option<f64>,
170 #[serde(
171 default,
172 skip_serializing_if = "Option::is_none",
173 deserialize_with = "deserialize_present"
174 )]
175 pub twist_rate_m_per_turn: Option<f64>,
176 #[serde(
177 default,
178 skip_serializing_if = "Option::is_none",
179 deserialize_with = "deserialize_present"
180 )]
181 pub twist_direction: Option<TwistDirectionV1>,
182}
183
184#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum TwistDirectionV1 {
188 Left,
189 #[default]
190 Right,
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195#[serde(deny_unknown_fields)]
196pub struct ShotV1 {
197 pub max_range_m: f64,
198 #[serde(
199 default,
200 skip_serializing_if = "Option::is_none",
201 deserialize_with = "deserialize_present"
202 )]
203 pub zero_distance_m: Option<f64>,
204 #[serde(
205 default,
206 skip_serializing_if = "Option::is_none",
207 deserialize_with = "deserialize_present"
208 )]
209 pub muzzle_angle_rad: Option<f64>,
210 #[serde(
211 default,
212 skip_serializing_if = "Option::is_none",
213 deserialize_with = "deserialize_present"
214 )]
215 pub aim_azimuth_rad: Option<f64>,
216 #[serde(
217 default,
218 skip_serializing_if = "Option::is_none",
219 deserialize_with = "deserialize_present"
220 )]
221 pub shot_azimuth_rad: Option<f64>,
222 #[serde(
223 default,
224 skip_serializing_if = "Option::is_none",
225 deserialize_with = "deserialize_present"
226 )]
227 pub shooting_angle_rad: Option<f64>,
228 #[serde(
229 default,
230 skip_serializing_if = "Option::is_none",
231 deserialize_with = "deserialize_present"
232 )]
233 pub cant_angle_rad: Option<f64>,
234 #[serde(
235 default,
236 skip_serializing_if = "Option::is_none",
237 deserialize_with = "deserialize_present"
238 )]
239 pub target_height_m: Option<f64>,
240 #[serde(
241 default,
242 skip_serializing_if = "Option::is_none",
243 deserialize_with = "deserialize_present"
244 )]
245 pub ground_threshold_m: Option<f64>,
246}
247
248#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
250#[serde(deny_unknown_fields)]
251pub struct AtmosphereV1 {
252 #[serde(
253 default,
254 skip_serializing_if = "Option::is_none",
255 deserialize_with = "deserialize_present"
256 )]
257 pub altitude_m: Option<f64>,
258 #[serde(
261 default,
262 skip_serializing_if = "Option::is_none",
263 deserialize_with = "deserialize_present"
264 )]
265 pub temperature_k: Option<f64>,
266 #[serde(
269 default,
270 skip_serializing_if = "Option::is_none",
271 deserialize_with = "deserialize_present"
272 )]
273 pub pressure_pa: Option<f64>,
274 #[serde(
275 default,
276 skip_serializing_if = "Option::is_none",
277 deserialize_with = "deserialize_present"
278 )]
279 pub relative_humidity: Option<f64>,
280 #[serde(
281 default,
282 skip_serializing_if = "Option::is_none",
283 deserialize_with = "deserialize_present"
284 )]
285 pub latitude_rad: Option<f64>,
286}
287
288#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
293#[serde(deny_unknown_fields)]
294pub struct WindV1 {
295 #[serde(
296 default,
297 skip_serializing_if = "Option::is_none",
298 deserialize_with = "deserialize_present"
299 )]
300 pub speed_mps: Option<f64>,
301 #[serde(
302 default,
303 skip_serializing_if = "Option::is_none",
304 deserialize_with = "deserialize_present"
305 )]
306 pub direction_from_rad: Option<f64>,
307 #[serde(
308 default,
309 skip_serializing_if = "Option::is_none",
310 deserialize_with = "deserialize_present"
311 )]
312 pub vertical_speed_mps: Option<f64>,
313 #[serde(
314 default,
315 skip_serializing_if = "Option::is_none",
316 deserialize_with = "deserialize_present"
317 )]
318 pub segments: Option<Vec<WindSegmentV1>>,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct WindSegmentV1 {
325 pub until_distance_m: f64,
326 pub speed_mps: f64,
327 pub direction_from_rad: f64,
328 #[serde(
329 default,
330 skip_serializing_if = "Option::is_none",
331 deserialize_with = "deserialize_present"
332 )]
333 pub vertical_speed_mps: Option<f64>,
334}
335
336#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
338#[serde(deny_unknown_fields)]
339pub struct SolverV1 {
340 #[serde(
341 default,
342 skip_serializing_if = "Option::is_none",
343 deserialize_with = "deserialize_present"
344 )]
345 pub method: Option<SolverMethodV1>,
346 #[serde(
348 default,
349 skip_serializing_if = "Option::is_none",
350 deserialize_with = "deserialize_present"
351 )]
352 pub time_step_s: Option<f64>,
353}
354
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
357#[serde(rename_all = "snake_case")]
358pub enum SolverMethodV1 {
359 Euler,
360 Rk4,
361 #[default]
362 Rk45,
363}
364
365#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
367#[serde(deny_unknown_fields)]
368pub struct EffectsV1 {
369 #[serde(
370 default,
371 skip_serializing_if = "Option::is_none",
372 deserialize_with = "deserialize_present"
373 )]
374 pub magnus: Option<bool>,
375 #[serde(
376 default,
377 skip_serializing_if = "Option::is_none",
378 deserialize_with = "deserialize_present"
379 )]
380 pub coriolis: Option<bool>,
381 #[serde(
382 default,
383 skip_serializing_if = "Option::is_none",
384 deserialize_with = "deserialize_present"
385 )]
386 pub enhanced_spin_drift: Option<bool>,
387}
388
389#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
391#[serde(deny_unknown_fields)]
392pub struct SamplingV1 {
393 #[serde(
394 default,
395 skip_serializing_if = "Option::is_none",
396 deserialize_with = "deserialize_present"
397 )]
398 pub interval_m: Option<f64>,
399}
400
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407#[serde(deny_unknown_fields)]
408pub struct ResolvedSolveRequestV1 {
409 pub schema_version: SchemaVersionV1,
410 pub projectile: ResolvedProjectileV1,
411 pub rifle: ResolvedRifleV1,
412 pub shot: ResolvedShotV1,
413 pub atmosphere: ResolvedAtmosphereV1,
414 pub wind: ResolvedWindV1,
415 pub solver: ResolvedSolverV1,
416 pub effects: ResolvedEffectsV1,
417 pub sampling: ResolvedSamplingV1,
418}
419
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422#[serde(deny_unknown_fields)]
423pub struct ResolvedProjectileV1 {
424 pub mass_kg: f64,
425 pub diameter_m: f64,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub length_m: Option<f64>,
428 pub drag_model: DragModelV1,
429 pub ballistic_coefficient: f64,
430}
431
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
434#[serde(deny_unknown_fields)]
435pub struct ResolvedRifleV1 {
436 pub muzzle_velocity_mps: f64,
437 pub sight_height_m: f64,
438 pub muzzle_height_m: f64,
439 pub twist_rate_m_per_turn: f64,
440 pub twist_direction: TwistDirectionV1,
441}
442
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448#[serde(deny_unknown_fields)]
449pub struct ResolvedShotV1 {
450 pub max_range_m: f64,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub zero_distance_m: Option<f64>,
453 pub muzzle_angle_rad: f64,
454 pub aim_azimuth_rad: f64,
455 pub shot_azimuth_rad: f64,
456 pub shooting_angle_rad: f64,
457 pub cant_angle_rad: f64,
458 pub target_height_m: f64,
459 pub ground_threshold_m: f64,
460}
461
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464#[serde(deny_unknown_fields)]
465pub struct ResolvedAtmosphereV1 {
466 pub altitude_m: f64,
467 pub temperature_k: f64,
468 pub pressure_pa: f64,
469 pub relative_humidity: f64,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub latitude_rad: Option<f64>,
472}
473
474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479#[serde(untagged)]
480pub enum ResolvedWindV1 {
481 Constant(ResolvedConstantWindV1),
482 Segmented(ResolvedSegmentedWindV1),
483}
484
485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
487#[serde(deny_unknown_fields)]
488pub struct ResolvedConstantWindV1 {
489 pub speed_mps: f64,
490 pub direction_from_rad: f64,
491 pub vertical_speed_mps: f64,
492}
493
494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496#[serde(deny_unknown_fields)]
497pub struct ResolvedSegmentedWindV1 {
498 pub segments: Vec<ResolvedWindSegmentV1>,
499}
500
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
503#[serde(deny_unknown_fields)]
504pub struct ResolvedWindSegmentV1 {
505 pub until_distance_m: f64,
506 pub speed_mps: f64,
507 pub direction_from_rad: f64,
508 pub vertical_speed_mps: f64,
509}
510
511#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
513#[serde(deny_unknown_fields)]
514pub struct ResolvedSolverV1 {
515 pub method: SolverMethodV1,
516 pub time_step_s: f64,
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(deny_unknown_fields)]
522pub struct ResolvedEffectsV1 {
523 pub magnus: bool,
524 pub coriolis: bool,
525 pub enhanced_spin_drift: bool,
526}
527
528#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
530#[serde(deny_unknown_fields)]
531pub struct ResolvedSamplingV1 {
532 pub interval_m: f64,
533}
534
535#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
537#[serde(deny_unknown_fields)]
538pub struct SolveSuccessV1 {
539 pub schema_version: SchemaVersionV1,
540 pub engine_version: String,
541 pub status: SuccessStatusV1,
542 pub resolved_request: ResolvedSolveRequestV1,
543 #[serde(default)]
544 pub assumptions: Vec<SolveNoticeV1>,
545 #[serde(default)]
546 pub warnings: Vec<SolveNoticeV1>,
547 pub summary: SolveSummaryV1,
548 #[serde(default, serialize_with = "serialize_solve_samples_v1")]
549 pub samples: Vec<TrajectorySampleV1>,
550}
551
552fn serialize_solve_samples_v1<S>(
553 samples: &[TrajectorySampleV1],
554 serializer: S,
555) -> Result<S::Ok, S::Error>
556where
557 S: Serializer,
558{
559 if samples.len() > MAX_SOLVE_JSON_SAMPLES_V1 {
560 return Err(serde::ser::Error::custom(format_args!(
561 "solve-json v1 response sample limit of {MAX_SOLVE_JSON_SAMPLES_V1} exceeded: response has {} samples",
562 samples.len()
563 )));
564 }
565 samples.serialize(serializer)
566}
567
568impl SolveSuccessV1 {
569 pub fn validate_for_serialization(&self) -> Result<(), SolveErrorEnvelopeV1> {
575 if self.samples.len() <= MAX_SOLVE_JSON_SAMPLES_V1 {
576 return Ok(());
577 }
578
579 Err(SolveErrorEnvelopeV1::new(
580 SolveErrorV1::new(
581 SolveErrorCodeV1::ResourceLimit,
582 format!(
583 "solve-json v1 response sample limit of {MAX_SOLVE_JSON_SAMPLES_V1} exceeded: response has {} samples",
584 self.samples.len()
585 ),
586 )
587 .at_path("$.sampling.interval_m"),
588 ))
589 }
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
594#[serde(rename_all = "snake_case")]
595pub enum SuccessStatusV1 {
596 Ok,
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(deny_unknown_fields)]
602pub struct SolveNoticeV1 {
603 pub code: String,
604 pub message: String,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub path: Option<String>,
607}
608
609#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611#[serde(deny_unknown_fields)]
612pub struct SolveSummaryV1 {
613 pub actual_range_m: f64,
614 pub maximum_height_m: f64,
617 pub time_of_flight_s: f64,
618 pub terminal_speed_mps: f64,
619 pub terminal_energy_j: f64,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub stability_factor: Option<f64>,
624 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub spin_drift_m: Option<f64>,
629 pub termination: TerminationReasonV1,
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "snake_case")]
635pub enum TerminationReasonV1 {
636 MaxRange,
637 GroundThreshold,
638 TimeLimit,
639 VelocityFloor,
640}
641
642#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
644#[serde(deny_unknown_fields)]
645pub struct TrajectorySampleV1 {
646 pub distance_m: f64,
647 pub time_s: f64,
648 pub speed_mps: f64,
649 pub energy_j: f64,
650 pub drop_m: f64,
652 pub windage_m: f64,
654 pub mach: f64,
655 #[serde(default)]
656 pub flags: Vec<SampleFlagV1>,
657}
658
659#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(rename_all = "snake_case")]
662pub enum SampleFlagV1 {
663 Transonic,
664 Subsonic,
665 Terminal,
666 GroundThreshold,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
671#[serde(deny_unknown_fields)]
672pub struct SolveErrorEnvelopeV1 {
673 pub schema_version: SchemaVersionV1,
674 pub status: ErrorStatusV1,
675 pub error: SolveErrorV1,
676}
677
678impl SolveErrorEnvelopeV1 {
679 pub fn new(error: SolveErrorV1) -> Self {
681 Self {
682 schema_version: SchemaVersionV1,
683 status: ErrorStatusV1::Error,
684 error,
685 }
686 }
687}
688
689#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
691#[serde(rename_all = "snake_case")]
692pub enum ErrorStatusV1 {
693 Error,
694}
695
696#[derive(Debug, Clone, PartialEq, Eq)]
702pub struct SolveErrorV1 {
703 pub code: SolveErrorCodeV1,
704 pub message: String,
705 location: SolveErrorLocationV1,
706}
707
708#[derive(Debug, Clone, PartialEq, Eq)]
709enum SolveErrorLocationV1 {
710 None,
711 Path(String),
712 Source {
713 line: NonZeroUsize,
714 column: NonZeroUsize,
715 },
716}
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
720pub enum SolveErrorLocationErrorV1 {
721 ZeroLine,
722 ZeroColumn,
723}
724
725impl fmt::Display for SolveErrorLocationErrorV1 {
726 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
727 match self {
728 Self::ZeroLine => formatter.write_str("error line must be one-based"),
729 Self::ZeroColumn => formatter.write_str("error column must be one-based"),
730 }
731 }
732}
733
734impl std::error::Error for SolveErrorLocationErrorV1 {}
735
736impl SolveErrorV1 {
737 pub fn new(code: SolveErrorCodeV1, message: impl Into<String>) -> Self {
739 Self {
740 code,
741 message: message.into(),
742 location: SolveErrorLocationV1::None,
743 }
744 }
745
746 pub fn at_path(mut self, path: impl Into<String>) -> Self {
748 self.location = SolveErrorLocationV1::Path(path.into());
749 self
750 }
751
752 pub fn at_location(
754 mut self,
755 line: usize,
756 column: usize,
757 ) -> Result<Self, SolveErrorLocationErrorV1> {
758 let line = NonZeroUsize::new(line).ok_or(SolveErrorLocationErrorV1::ZeroLine)?;
759 let column = NonZeroUsize::new(column).ok_or(SolveErrorLocationErrorV1::ZeroColumn)?;
760 self.location = SolveErrorLocationV1::Source { line, column };
761 Ok(self)
762 }
763
764 fn at_parser_location(mut self, line: usize, column: usize) -> Self {
769 self.location = SolveErrorLocationV1::Source {
770 line: NonZeroUsize::new(line).unwrap_or(NonZeroUsize::MIN),
771 column: NonZeroUsize::new(column).unwrap_or(NonZeroUsize::MIN),
772 };
773 self
774 }
775
776 pub fn path(&self) -> Option<&str> {
778 match &self.location {
779 SolveErrorLocationV1::Path(path) => Some(path),
780 SolveErrorLocationV1::None | SolveErrorLocationV1::Source { .. } => None,
781 }
782 }
783
784 pub fn line(&self) -> Option<usize> {
786 match &self.location {
787 SolveErrorLocationV1::Source { line, .. } => Some(line.get()),
788 SolveErrorLocationV1::None | SolveErrorLocationV1::Path(_) => None,
789 }
790 }
791
792 pub fn column(&self) -> Option<usize> {
794 match &self.location {
795 SolveErrorLocationV1::Source { column, .. } => Some(column.get()),
796 SolveErrorLocationV1::None | SolveErrorLocationV1::Path(_) => None,
797 }
798 }
799}
800
801impl Serialize for SolveErrorV1 {
802 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
803 where
804 S: Serializer,
805 {
806 let mut state = serializer.serialize_struct("SolveErrorV1", 5)?;
807 state.serialize_field("code", &self.code)?;
808 state.serialize_field("message", &self.message)?;
809 state.serialize_field("path", &self.path())?;
810 state.serialize_field("line", &self.line())?;
811 state.serialize_field("column", &self.column())?;
812 state.end()
813 }
814}
815
816impl<'de> Deserialize<'de> for SolveErrorV1 {
817 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
818 where
819 D: Deserializer<'de>,
820 {
821 #[derive(Deserialize)]
822 #[serde(deny_unknown_fields)]
823 struct SolveErrorWireV1 {
824 code: SolveErrorCodeV1,
825 message: String,
826 path: Option<String>,
827 line: Option<usize>,
828 column: Option<usize>,
829 }
830
831 let wire = SolveErrorWireV1::deserialize(deserializer)?;
832 let location = match (wire.path, wire.line, wire.column) {
833 (None, None, None) => SolveErrorLocationV1::None,
834 (Some(path), None, None) => SolveErrorLocationV1::Path(path),
835 (None, Some(line), Some(column)) => {
836 let line = NonZeroUsize::new(line)
837 .ok_or_else(|| de::Error::custom("error line must be one-based"))?;
838 let column = NonZeroUsize::new(column)
839 .ok_or_else(|| de::Error::custom("error column must be one-based"))?;
840 SolveErrorLocationV1::Source { line, column }
841 }
842 _ => {
843 return Err(de::Error::custom(
844 "error location must contain either path or both line and column",
845 ));
846 }
847 };
848
849 Ok(Self {
850 code: wire.code,
851 message: wire.message,
852 location,
853 })
854 }
855}
856
857#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
859#[serde(rename_all = "snake_case")]
860pub enum SolveErrorCodeV1 {
861 InvalidJson,
862 UnsupportedSchemaVersion,
863 UnknownField,
864 MissingField,
865 InvalidValue,
866 ConflictingFields,
867 ResourceLimit,
868 SolveFailed,
869 IoError,
870 InternalError,
871}
872
873pub fn decode_solve_request_v1(input: &str) -> Result<SolveRequestV1, SolveErrorEnvelopeV1> {
880 let value: Value = serde_json::from_str(input).map_err(|error| {
881 let error = SolveErrorV1::new(SolveErrorCodeV1::InvalidJson, error.to_string())
882 .at_parser_location(error.line(), error.column());
883 envelope(error)
884 })?;
885
886 validate_request_shape(&value)?;
887
888 serde_json::from_value(value).map_err(|error| {
889 envelope(SolveErrorV1::new(SolveErrorCodeV1::InvalidValue, error.to_string()).at_path("$"))
890 })
891}
892
893fn envelope(error: SolveErrorV1) -> SolveErrorEnvelopeV1 {
894 SolveErrorEnvelopeV1::new(error)
895}
896
897fn protocol_error(
898 code: SolveErrorCodeV1,
899 message: impl Into<String>,
900 path: impl Into<String>,
901) -> SolveErrorEnvelopeV1 {
902 envelope(SolveErrorV1::new(code, message).at_path(path))
903}
904
905fn validate_request_shape(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
906 let root = require_object(value, "$")?;
907
908 validate_schema_version(root)?;
912 validate_members(
913 root,
914 "$",
915 &[
916 "schema_version",
917 "projectile",
918 "rifle",
919 "shot",
920 "atmosphere",
921 "wind",
922 "solver",
923 "effects",
924 "sampling",
925 ],
926 &[
927 "schema_version",
928 "projectile",
929 "rifle",
930 "shot",
931 "atmosphere",
932 "wind",
933 "solver",
934 "effects",
935 "sampling",
936 ],
937 )?;
938
939 validate_projectile(required_value(root, "projectile", "$")?)?;
940 validate_rifle(required_value(root, "rifle", "$")?)?;
941 validate_shot(required_value(root, "shot", "$")?)?;
942 validate_atmosphere(required_value(root, "atmosphere", "$")?)?;
943 validate_wind(required_value(root, "wind", "$")?)?;
944 validate_solver(required_value(root, "solver", "$")?)?;
945 validate_effects(required_value(root, "effects", "$")?)?;
946 validate_sampling(required_value(root, "sampling", "$")?)?;
947 Ok(())
948}
949
950fn validate_schema_version(root: &Map<String, Value>) -> Result<(), SolveErrorEnvelopeV1> {
951 let value = required_value(root, "schema_version", "$")?;
952 let version = if let Some(version) = value.as_i64() {
953 i128::from(version)
954 } else if let Some(version) = value.as_u64() {
955 i128::from(version)
956 } else {
957 return Err(protocol_error(
958 SolveErrorCodeV1::InvalidValue,
959 "schema_version must be the integer 1",
960 "$.schema_version",
961 ));
962 };
963 if version != i128::from(SOLVE_JSON_SCHEMA_VERSION_V1) {
964 return Err(protocol_error(
965 SolveErrorCodeV1::UnsupportedSchemaVersion,
966 format!(
967 "unsupported schema_version {version}; expected {SOLVE_JSON_SCHEMA_VERSION_V1}"
968 ),
969 "$.schema_version",
970 ));
971 }
972 Ok(())
973}
974
975fn validate_projectile(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
976 let path = "$.projectile";
977 let object = require_object(value, path)?;
978 validate_members(
979 object,
980 path,
981 &[
982 "mass_kg",
983 "diameter_m",
984 "length_m",
985 "drag_model",
986 "ballistic_coefficient",
987 ],
988 &[
989 "mass_kg",
990 "diameter_m",
991 "drag_model",
992 "ballistic_coefficient",
993 ],
994 )?;
995 validate_required_numbers(
996 object,
997 path,
998 &["mass_kg", "diameter_m", "ballistic_coefficient"],
999 )?;
1000 validate_optional_number(object, path, "length_m")?;
1001 validate_string_enum(
1002 required_value(object, "drag_model", path)?,
1003 "$.projectile.drag_model",
1004 &["G1", "G6", "G7", "G8"],
1005 )
1006}
1007
1008fn validate_rifle(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1009 let path = "$.rifle";
1010 let object = require_object(value, path)?;
1011 validate_members(
1012 object,
1013 path,
1014 &[
1015 "muzzle_velocity_mps",
1016 "sight_height_m",
1017 "muzzle_height_m",
1018 "twist_rate_m_per_turn",
1019 "twist_direction",
1020 ],
1021 &["muzzle_velocity_mps"],
1022 )?;
1023 validate_required_numbers(object, path, &["muzzle_velocity_mps"])?;
1024 validate_optional_numbers(
1025 object,
1026 path,
1027 &["sight_height_m", "muzzle_height_m", "twist_rate_m_per_turn"],
1028 )?;
1029 if let Some(direction) = object.get("twist_direction") {
1030 validate_string_enum(direction, "$.rifle.twist_direction", &["left", "right"])?;
1031 }
1032 Ok(())
1033}
1034
1035fn validate_shot(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1036 let path = "$.shot";
1037 let object = require_object(value, path)?;
1038 validate_members(
1039 object,
1040 path,
1041 &[
1042 "max_range_m",
1043 "zero_distance_m",
1044 "muzzle_angle_rad",
1045 "aim_azimuth_rad",
1046 "shot_azimuth_rad",
1047 "shooting_angle_rad",
1048 "cant_angle_rad",
1049 "target_height_m",
1050 "ground_threshold_m",
1051 ],
1052 &["max_range_m"],
1053 )?;
1054 validate_required_numbers(object, path, &["max_range_m"])?;
1055 validate_optional_number(object, path, "zero_distance_m")?;
1056 validate_optional_number(object, path, "muzzle_angle_rad")?;
1057 validate_optional_numbers(
1058 object,
1059 path,
1060 &[
1061 "aim_azimuth_rad",
1062 "shot_azimuth_rad",
1063 "shooting_angle_rad",
1064 "cant_angle_rad",
1065 "target_height_m",
1066 "ground_threshold_m",
1067 ],
1068 )
1069}
1070
1071fn validate_atmosphere(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1072 let path = "$.atmosphere";
1073 let object = require_object(value, path)?;
1074 validate_members(
1075 object,
1076 path,
1077 &[
1078 "altitude_m",
1079 "temperature_k",
1080 "pressure_pa",
1081 "relative_humidity",
1082 "latitude_rad",
1083 ],
1084 &[],
1085 )?;
1086 validate_optional_numbers(
1087 object,
1088 path,
1089 &[
1090 "altitude_m",
1091 "temperature_k",
1092 "pressure_pa",
1093 "relative_humidity",
1094 "latitude_rad",
1095 ],
1096 )
1097}
1098
1099fn validate_wind(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1100 let path = "$.wind";
1101 let object = require_object(value, path)?;
1102 validate_members(
1103 object,
1104 path,
1105 &[
1106 "speed_mps",
1107 "direction_from_rad",
1108 "vertical_speed_mps",
1109 "segments",
1110 ],
1111 &[],
1112 )?;
1113 for field in ["speed_mps", "direction_from_rad", "vertical_speed_mps"] {
1114 validate_optional_number(object, path, field)?;
1115 }
1116
1117 if let Some(segments) = object.get("segments") {
1118 let Some(segments) = segments.as_array() else {
1119 return Err(protocol_error(
1120 SolveErrorCodeV1::InvalidValue,
1121 "segments must be an array",
1122 "$.wind.segments",
1123 ));
1124 };
1125 for (index, segment) in segments.iter().enumerate() {
1126 let segment_path = format!("$.wind.segments[{index}]");
1127 let segment = require_object(segment, &segment_path)?;
1128 validate_members(
1129 segment,
1130 &segment_path,
1131 &[
1132 "until_distance_m",
1133 "speed_mps",
1134 "direction_from_rad",
1135 "vertical_speed_mps",
1136 ],
1137 &["until_distance_m", "speed_mps", "direction_from_rad"],
1138 )?;
1139 validate_required_numbers(
1140 segment,
1141 &segment_path,
1142 &["until_distance_m", "speed_mps", "direction_from_rad"],
1143 )?;
1144 validate_optional_number(segment, &segment_path, "vertical_speed_mps")?;
1145 }
1146 }
1147 Ok(())
1148}
1149
1150fn validate_solver(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1151 let path = "$.solver";
1152 let object = require_object(value, path)?;
1153 validate_members(object, path, &["method", "time_step_s"], &[])?;
1154 validate_optional_number(object, path, "time_step_s")?;
1155 if let Some(method) = object.get("method") {
1156 validate_string_enum(method, "$.solver.method", &["euler", "rk4", "rk45"])?;
1157 }
1158 Ok(())
1159}
1160
1161fn validate_effects(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1162 let path = "$.effects";
1163 let object = require_object(value, path)?;
1164 validate_members(
1165 object,
1166 path,
1167 &["magnus", "coriolis", "enhanced_spin_drift"],
1168 &[],
1169 )?;
1170 validate_optional_booleans(object, path, &["magnus", "coriolis", "enhanced_spin_drift"])?;
1171
1172 if object.get("magnus").and_then(Value::as_bool) == Some(true)
1173 && object.get("enhanced_spin_drift").and_then(Value::as_bool) == Some(true)
1174 {
1175 return Err(protocol_error(
1176 SolveErrorCodeV1::ConflictingFields,
1177 "magnus and enhanced_spin_drift cannot both be enabled",
1178 "$.effects",
1179 ));
1180 }
1181
1182 Ok(())
1183}
1184
1185fn validate_sampling(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1186 let path = "$.sampling";
1187 let object = require_object(value, path)?;
1188 validate_members(object, path, &["interval_m"], &[])?;
1189 validate_optional_number(object, path, "interval_m")
1190}
1191
1192fn require_object<'a>(
1193 value: &'a Value,
1194 path: &str,
1195) -> Result<&'a Map<String, Value>, SolveErrorEnvelopeV1> {
1196 value
1197 .as_object()
1198 .ok_or_else(|| protocol_error(SolveErrorCodeV1::InvalidValue, "expected an object", path))
1199}
1200
1201fn required_value<'a>(
1202 object: &'a Map<String, Value>,
1203 field: &str,
1204 parent_path: &str,
1205) -> Result<&'a Value, SolveErrorEnvelopeV1> {
1206 object.get(field).ok_or_else(|| {
1207 protocol_error(
1208 SolveErrorCodeV1::MissingField,
1209 format!("missing required field `{field}`"),
1210 child_path(parent_path, field),
1211 )
1212 })
1213}
1214
1215fn validate_members(
1216 object: &Map<String, Value>,
1217 path: &str,
1218 allowed: &[&str],
1219 required: &[&str],
1220) -> Result<(), SolveErrorEnvelopeV1> {
1221 if let Some(field) = object
1222 .keys()
1223 .find(|field| !allowed.contains(&field.as_str()))
1224 {
1225 return Err(protocol_error(
1226 SolveErrorCodeV1::UnknownField,
1227 format!("unknown field `{field}`"),
1228 child_path(path, field),
1229 ));
1230 }
1231
1232 if let Some(field) = required.iter().find(|field| !object.contains_key(**field)) {
1233 return Err(protocol_error(
1234 SolveErrorCodeV1::MissingField,
1235 format!("missing required field `{field}`"),
1236 child_path(path, field),
1237 ));
1238 }
1239 Ok(())
1240}
1241
1242fn validate_string_enum(
1243 value: &Value,
1244 path: &str,
1245 allowed: &[&str],
1246) -> Result<(), SolveErrorEnvelopeV1> {
1247 let Some(value) = value.as_str() else {
1248 return Err(protocol_error(
1249 SolveErrorCodeV1::InvalidValue,
1250 "expected a string enum value",
1251 path,
1252 ));
1253 };
1254 if !allowed.contains(&value) {
1255 return Err(protocol_error(
1256 SolveErrorCodeV1::InvalidValue,
1257 format!(
1258 "invalid value `{value}`; expected one of {}",
1259 allowed.join(", ")
1260 ),
1261 path,
1262 ));
1263 }
1264 Ok(())
1265}
1266
1267fn validate_required_numbers(
1268 object: &Map<String, Value>,
1269 parent_path: &str,
1270 fields: &[&str],
1271) -> Result<(), SolveErrorEnvelopeV1> {
1272 for field in fields {
1273 let value = required_value(object, field, parent_path)?;
1274 validate_number(value, &child_path(parent_path, field))?;
1275 }
1276 Ok(())
1277}
1278
1279fn validate_optional_numbers(
1280 object: &Map<String, Value>,
1281 parent_path: &str,
1282 fields: &[&str],
1283) -> Result<(), SolveErrorEnvelopeV1> {
1284 for field in fields {
1285 validate_optional_number(object, parent_path, field)?;
1286 }
1287 Ok(())
1288}
1289
1290fn validate_optional_number(
1291 object: &Map<String, Value>,
1292 parent_path: &str,
1293 field: &str,
1294) -> Result<(), SolveErrorEnvelopeV1> {
1295 if let Some(value) = object.get(field) {
1296 validate_number(value, &child_path(parent_path, field))?;
1297 }
1298 Ok(())
1299}
1300
1301fn validate_number(value: &Value, path: &str) -> Result<(), SolveErrorEnvelopeV1> {
1302 if value.is_number() {
1303 Ok(())
1304 } else {
1305 Err(protocol_error(
1306 SolveErrorCodeV1::InvalidValue,
1307 "expected a number",
1308 path,
1309 ))
1310 }
1311}
1312
1313fn validate_optional_booleans(
1314 object: &Map<String, Value>,
1315 parent_path: &str,
1316 fields: &[&str],
1317) -> Result<(), SolveErrorEnvelopeV1> {
1318 for field in fields {
1319 if let Some(value) = object.get(*field) {
1320 if !value.is_boolean() {
1321 return Err(protocol_error(
1322 SolveErrorCodeV1::InvalidValue,
1323 "expected a boolean",
1324 child_path(parent_path, field),
1325 ));
1326 }
1327 }
1328 }
1329 Ok(())
1330}
1331
1332fn child_path(parent: &str, field: &str) -> String {
1333 format!("{parent}.{field}")
1334}