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(
272 default,
273 skip_serializing_if = "Option::is_none",
274 deserialize_with = "deserialize_present"
275 )]
276 pub pressure_pa: Option<f64>,
277 #[serde(
283 default,
284 skip_serializing_if = "Option::is_none",
285 deserialize_with = "deserialize_present"
286 )]
287 pub pressure_reference: Option<PressureReferenceV1>,
288 #[serde(
289 default,
290 skip_serializing_if = "Option::is_none",
291 deserialize_with = "deserialize_present"
292 )]
293 pub relative_humidity: Option<f64>,
294 #[serde(
295 default,
296 skip_serializing_if = "Option::is_none",
297 deserialize_with = "deserialize_present"
298 )]
299 pub latitude_rad: Option<f64>,
300}
301
302#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "snake_case")]
307pub enum PressureReferenceV1 {
308 #[default]
309 Absolute,
310 Qnh,
311}
312
313#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
318#[serde(deny_unknown_fields)]
319pub struct WindV1 {
320 #[serde(
321 default,
322 skip_serializing_if = "Option::is_none",
323 deserialize_with = "deserialize_present"
324 )]
325 pub speed_mps: Option<f64>,
326 #[serde(
327 default,
328 skip_serializing_if = "Option::is_none",
329 deserialize_with = "deserialize_present"
330 )]
331 pub direction_from_rad: Option<f64>,
332 #[serde(
333 default,
334 skip_serializing_if = "Option::is_none",
335 deserialize_with = "deserialize_present"
336 )]
337 pub vertical_speed_mps: Option<f64>,
338 #[serde(
339 default,
340 skip_serializing_if = "Option::is_none",
341 deserialize_with = "deserialize_present"
342 )]
343 pub segments: Option<Vec<WindSegmentV1>>,
344}
345
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
348#[serde(deny_unknown_fields)]
349pub struct WindSegmentV1 {
350 pub until_distance_m: f64,
351 pub speed_mps: f64,
352 pub direction_from_rad: f64,
353 #[serde(
354 default,
355 skip_serializing_if = "Option::is_none",
356 deserialize_with = "deserialize_present"
357 )]
358 pub vertical_speed_mps: Option<f64>,
359}
360
361#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
363#[serde(deny_unknown_fields)]
364pub struct SolverV1 {
365 #[serde(
366 default,
367 skip_serializing_if = "Option::is_none",
368 deserialize_with = "deserialize_present"
369 )]
370 pub method: Option<SolverMethodV1>,
371 #[serde(
373 default,
374 skip_serializing_if = "Option::is_none",
375 deserialize_with = "deserialize_present"
376 )]
377 pub time_step_s: Option<f64>,
378}
379
380#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(rename_all = "snake_case")]
383pub enum SolverMethodV1 {
384 Euler,
385 Rk4,
386 #[default]
387 Rk45,
388}
389
390#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(deny_unknown_fields)]
393pub struct EffectsV1 {
394 #[serde(
395 default,
396 skip_serializing_if = "Option::is_none",
397 deserialize_with = "deserialize_present"
398 )]
399 pub magnus: Option<bool>,
400 #[serde(
401 default,
402 skip_serializing_if = "Option::is_none",
403 deserialize_with = "deserialize_present"
404 )]
405 pub coriolis: Option<bool>,
406 #[serde(
407 default,
408 skip_serializing_if = "Option::is_none",
409 deserialize_with = "deserialize_present"
410 )]
411 pub enhanced_spin_drift: Option<bool>,
412}
413
414#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
416#[serde(deny_unknown_fields)]
417pub struct SamplingV1 {
418 #[serde(
419 default,
420 skip_serializing_if = "Option::is_none",
421 deserialize_with = "deserialize_present"
422 )]
423 pub interval_m: Option<f64>,
424}
425
426#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432#[serde(deny_unknown_fields)]
433pub struct ResolvedSolveRequestV1 {
434 pub schema_version: SchemaVersionV1,
435 pub projectile: ResolvedProjectileV1,
436 pub rifle: ResolvedRifleV1,
437 pub shot: ResolvedShotV1,
438 pub atmosphere: ResolvedAtmosphereV1,
439 pub wind: ResolvedWindV1,
440 pub solver: ResolvedSolverV1,
441 pub effects: ResolvedEffectsV1,
442 pub sampling: ResolvedSamplingV1,
443}
444
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
447#[serde(deny_unknown_fields)]
448pub struct ResolvedProjectileV1 {
449 pub mass_kg: f64,
450 pub diameter_m: f64,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub length_m: Option<f64>,
453 pub drag_model: DragModelV1,
454 pub ballistic_coefficient: f64,
455}
456
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
459#[serde(deny_unknown_fields)]
460pub struct ResolvedRifleV1 {
461 pub muzzle_velocity_mps: f64,
462 pub sight_height_m: f64,
463 pub muzzle_height_m: f64,
464 pub twist_rate_m_per_turn: f64,
465 pub twist_direction: TwistDirectionV1,
466}
467
468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473#[serde(deny_unknown_fields)]
474pub struct ResolvedShotV1 {
475 pub max_range_m: f64,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub zero_distance_m: Option<f64>,
478 pub muzzle_angle_rad: f64,
479 pub aim_azimuth_rad: f64,
480 pub shot_azimuth_rad: f64,
481 pub shooting_angle_rad: f64,
482 pub cant_angle_rad: f64,
483 pub target_height_m: f64,
484 pub ground_threshold_m: f64,
485}
486
487#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
489#[serde(deny_unknown_fields)]
490pub struct ResolvedAtmosphereV1 {
491 pub altitude_m: f64,
492 pub temperature_k: f64,
493 pub pressure_pa: f64,
494 pub relative_humidity: f64,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub latitude_rad: Option<f64>,
497}
498
499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
504#[serde(untagged)]
505pub enum ResolvedWindV1 {
506 Constant(ResolvedConstantWindV1),
507 Segmented(ResolvedSegmentedWindV1),
508}
509
510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
512#[serde(deny_unknown_fields)]
513pub struct ResolvedConstantWindV1 {
514 pub speed_mps: f64,
515 pub direction_from_rad: f64,
516 pub vertical_speed_mps: f64,
517}
518
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
521#[serde(deny_unknown_fields)]
522pub struct ResolvedSegmentedWindV1 {
523 pub segments: Vec<ResolvedWindSegmentV1>,
524}
525
526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528#[serde(deny_unknown_fields)]
529pub struct ResolvedWindSegmentV1 {
530 pub until_distance_m: f64,
531 pub speed_mps: f64,
532 pub direction_from_rad: f64,
533 pub vertical_speed_mps: f64,
534}
535
536#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538#[serde(deny_unknown_fields)]
539pub struct ResolvedSolverV1 {
540 pub method: SolverMethodV1,
541 pub time_step_s: f64,
542}
543
544#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546#[serde(deny_unknown_fields)]
547pub struct ResolvedEffectsV1 {
548 pub magnus: bool,
549 pub coriolis: bool,
550 pub enhanced_spin_drift: bool,
551}
552
553#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
555#[serde(deny_unknown_fields)]
556pub struct ResolvedSamplingV1 {
557 pub interval_m: f64,
558}
559
560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
562#[serde(deny_unknown_fields)]
563pub struct SolveSuccessV1 {
564 pub schema_version: SchemaVersionV1,
565 pub engine_version: String,
566 pub status: SuccessStatusV1,
567 pub resolved_request: ResolvedSolveRequestV1,
568 #[serde(default)]
569 pub assumptions: Vec<SolveNoticeV1>,
570 #[serde(default)]
571 pub warnings: Vec<SolveNoticeV1>,
572 pub summary: SolveSummaryV1,
573 #[serde(default, serialize_with = "serialize_solve_samples_v1")]
574 pub samples: Vec<TrajectorySampleV1>,
575}
576
577fn serialize_solve_samples_v1<S>(
578 samples: &[TrajectorySampleV1],
579 serializer: S,
580) -> Result<S::Ok, S::Error>
581where
582 S: Serializer,
583{
584 if samples.len() > MAX_SOLVE_JSON_SAMPLES_V1 {
585 return Err(serde::ser::Error::custom(format_args!(
586 "solve-json v1 response sample limit of {MAX_SOLVE_JSON_SAMPLES_V1} exceeded: response has {} samples",
587 samples.len()
588 )));
589 }
590 samples.serialize(serializer)
591}
592
593impl SolveSuccessV1 {
594 pub fn validate_for_serialization(&self) -> Result<(), SolveErrorEnvelopeV1> {
600 if self.samples.len() <= MAX_SOLVE_JSON_SAMPLES_V1 {
601 return Ok(());
602 }
603
604 Err(SolveErrorEnvelopeV1::new(
605 SolveErrorV1::new(
606 SolveErrorCodeV1::ResourceLimit,
607 format!(
608 "solve-json v1 response sample limit of {MAX_SOLVE_JSON_SAMPLES_V1} exceeded: response has {} samples",
609 self.samples.len()
610 ),
611 )
612 .at_path("$.sampling.interval_m"),
613 ))
614 }
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
619#[serde(rename_all = "snake_case")]
620pub enum SuccessStatusV1 {
621 Ok,
622}
623
624#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
626#[serde(deny_unknown_fields)]
627pub struct SolveNoticeV1 {
628 pub code: String,
629 pub message: String,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub path: Option<String>,
632}
633
634#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct SolveSummaryV1 {
638 pub actual_range_m: f64,
639 pub maximum_height_m: f64,
642 pub time_of_flight_s: f64,
643 pub terminal_speed_mps: f64,
644 pub terminal_energy_j: f64,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
648 pub stability_factor: Option<f64>,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub spin_drift_m: Option<f64>,
654 pub termination: TerminationReasonV1,
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "snake_case")]
660pub enum TerminationReasonV1 {
661 MaxRange,
662 GroundThreshold,
663 TimeLimit,
664 VelocityFloor,
665}
666
667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
669#[serde(deny_unknown_fields)]
670pub struct TrajectorySampleV1 {
671 pub distance_m: f64,
672 pub time_s: f64,
673 pub speed_mps: f64,
674 pub energy_j: f64,
675 pub drop_m: f64,
677 pub windage_m: f64,
679 pub mach: f64,
680 #[serde(default)]
681 pub flags: Vec<SampleFlagV1>,
682}
683
684#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
686#[serde(rename_all = "snake_case")]
687pub enum SampleFlagV1 {
688 Transonic,
689 Subsonic,
690 Terminal,
691 GroundThreshold,
692}
693
694#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(deny_unknown_fields)]
697pub struct SolveErrorEnvelopeV1 {
698 pub schema_version: SchemaVersionV1,
699 pub status: ErrorStatusV1,
700 pub error: SolveErrorV1,
701}
702
703impl SolveErrorEnvelopeV1 {
704 pub fn new(error: SolveErrorV1) -> Self {
706 Self {
707 schema_version: SchemaVersionV1,
708 status: ErrorStatusV1::Error,
709 error,
710 }
711 }
712}
713
714#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(rename_all = "snake_case")]
717pub enum ErrorStatusV1 {
718 Error,
719}
720
721#[derive(Debug, Clone, PartialEq, Eq)]
727pub struct SolveErrorV1 {
728 pub code: SolveErrorCodeV1,
729 pub message: String,
730 location: SolveErrorLocationV1,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq)]
734enum SolveErrorLocationV1 {
735 None,
736 Path(String),
737 Source {
738 line: NonZeroUsize,
739 column: NonZeroUsize,
740 },
741}
742
743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
745pub enum SolveErrorLocationErrorV1 {
746 ZeroLine,
747 ZeroColumn,
748}
749
750impl fmt::Display for SolveErrorLocationErrorV1 {
751 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
752 match self {
753 Self::ZeroLine => formatter.write_str("error line must be one-based"),
754 Self::ZeroColumn => formatter.write_str("error column must be one-based"),
755 }
756 }
757}
758
759impl std::error::Error for SolveErrorLocationErrorV1 {}
760
761impl SolveErrorV1 {
762 pub fn new(code: SolveErrorCodeV1, message: impl Into<String>) -> Self {
764 Self {
765 code,
766 message: message.into(),
767 location: SolveErrorLocationV1::None,
768 }
769 }
770
771 pub fn at_path(mut self, path: impl Into<String>) -> Self {
773 self.location = SolveErrorLocationV1::Path(path.into());
774 self
775 }
776
777 pub fn at_location(
779 mut self,
780 line: usize,
781 column: usize,
782 ) -> Result<Self, SolveErrorLocationErrorV1> {
783 let line = NonZeroUsize::new(line).ok_or(SolveErrorLocationErrorV1::ZeroLine)?;
784 let column = NonZeroUsize::new(column).ok_or(SolveErrorLocationErrorV1::ZeroColumn)?;
785 self.location = SolveErrorLocationV1::Source { line, column };
786 Ok(self)
787 }
788
789 fn at_parser_location(mut self, line: usize, column: usize) -> Self {
794 self.location = SolveErrorLocationV1::Source {
795 line: NonZeroUsize::new(line).unwrap_or(NonZeroUsize::MIN),
796 column: NonZeroUsize::new(column).unwrap_or(NonZeroUsize::MIN),
797 };
798 self
799 }
800
801 pub fn path(&self) -> Option<&str> {
803 match &self.location {
804 SolveErrorLocationV1::Path(path) => Some(path),
805 SolveErrorLocationV1::None | SolveErrorLocationV1::Source { .. } => None,
806 }
807 }
808
809 pub fn line(&self) -> Option<usize> {
811 match &self.location {
812 SolveErrorLocationV1::Source { line, .. } => Some(line.get()),
813 SolveErrorLocationV1::None | SolveErrorLocationV1::Path(_) => None,
814 }
815 }
816
817 pub fn column(&self) -> Option<usize> {
819 match &self.location {
820 SolveErrorLocationV1::Source { column, .. } => Some(column.get()),
821 SolveErrorLocationV1::None | SolveErrorLocationV1::Path(_) => None,
822 }
823 }
824}
825
826impl Serialize for SolveErrorV1 {
827 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
828 where
829 S: Serializer,
830 {
831 let mut state = serializer.serialize_struct("SolveErrorV1", 5)?;
832 state.serialize_field("code", &self.code)?;
833 state.serialize_field("message", &self.message)?;
834 state.serialize_field("path", &self.path())?;
835 state.serialize_field("line", &self.line())?;
836 state.serialize_field("column", &self.column())?;
837 state.end()
838 }
839}
840
841impl<'de> Deserialize<'de> for SolveErrorV1 {
842 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
843 where
844 D: Deserializer<'de>,
845 {
846 #[derive(Deserialize)]
847 #[serde(deny_unknown_fields)]
848 struct SolveErrorWireV1 {
849 code: SolveErrorCodeV1,
850 message: String,
851 path: Option<String>,
852 line: Option<usize>,
853 column: Option<usize>,
854 }
855
856 let wire = SolveErrorWireV1::deserialize(deserializer)?;
857 let location = match (wire.path, wire.line, wire.column) {
858 (None, None, None) => SolveErrorLocationV1::None,
859 (Some(path), None, None) => SolveErrorLocationV1::Path(path),
860 (None, Some(line), Some(column)) => {
861 let line = NonZeroUsize::new(line)
862 .ok_or_else(|| de::Error::custom("error line must be one-based"))?;
863 let column = NonZeroUsize::new(column)
864 .ok_or_else(|| de::Error::custom("error column must be one-based"))?;
865 SolveErrorLocationV1::Source { line, column }
866 }
867 _ => {
868 return Err(de::Error::custom(
869 "error location must contain either path or both line and column",
870 ));
871 }
872 };
873
874 Ok(Self {
875 code: wire.code,
876 message: wire.message,
877 location,
878 })
879 }
880}
881
882#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
884#[serde(rename_all = "snake_case")]
885pub enum SolveErrorCodeV1 {
886 InvalidJson,
887 UnsupportedSchemaVersion,
888 UnknownField,
889 MissingField,
890 InvalidValue,
891 ConflictingFields,
892 ResourceLimit,
893 SolveFailed,
894 IoError,
895 InternalError,
896}
897
898pub fn decode_solve_request_v1(input: &str) -> Result<SolveRequestV1, SolveErrorEnvelopeV1> {
905 let value: Value = serde_json::from_str(input).map_err(|error| {
906 let error = SolveErrorV1::new(SolveErrorCodeV1::InvalidJson, error.to_string())
907 .at_parser_location(error.line(), error.column());
908 envelope(error)
909 })?;
910
911 validate_request_shape(&value)?;
912
913 serde_json::from_value(value).map_err(|error| {
914 envelope(SolveErrorV1::new(SolveErrorCodeV1::InvalidValue, error.to_string()).at_path("$"))
915 })
916}
917
918fn envelope(error: SolveErrorV1) -> SolveErrorEnvelopeV1 {
919 SolveErrorEnvelopeV1::new(error)
920}
921
922fn protocol_error(
923 code: SolveErrorCodeV1,
924 message: impl Into<String>,
925 path: impl Into<String>,
926) -> SolveErrorEnvelopeV1 {
927 envelope(SolveErrorV1::new(code, message).at_path(path))
928}
929
930fn validate_request_shape(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
931 let root = require_object(value, "$")?;
932
933 validate_schema_version(root)?;
937 validate_members(
938 root,
939 "$",
940 &[
941 "schema_version",
942 "projectile",
943 "rifle",
944 "shot",
945 "atmosphere",
946 "wind",
947 "solver",
948 "effects",
949 "sampling",
950 ],
951 &[
952 "schema_version",
953 "projectile",
954 "rifle",
955 "shot",
956 "atmosphere",
957 "wind",
958 "solver",
959 "effects",
960 "sampling",
961 ],
962 )?;
963
964 validate_projectile(required_value(root, "projectile", "$")?)?;
965 validate_rifle(required_value(root, "rifle", "$")?)?;
966 validate_shot(required_value(root, "shot", "$")?)?;
967 validate_atmosphere(required_value(root, "atmosphere", "$")?)?;
968 validate_wind(required_value(root, "wind", "$")?)?;
969 validate_solver(required_value(root, "solver", "$")?)?;
970 validate_effects(required_value(root, "effects", "$")?)?;
971 validate_sampling(required_value(root, "sampling", "$")?)?;
972 Ok(())
973}
974
975fn validate_schema_version(root: &Map<String, Value>) -> Result<(), SolveErrorEnvelopeV1> {
976 let value = required_value(root, "schema_version", "$")?;
977 let version = if let Some(version) = value.as_i64() {
978 i128::from(version)
979 } else if let Some(version) = value.as_u64() {
980 i128::from(version)
981 } else {
982 return Err(protocol_error(
983 SolveErrorCodeV1::InvalidValue,
984 "schema_version must be the integer 1",
985 "$.schema_version",
986 ));
987 };
988 if version != i128::from(SOLVE_JSON_SCHEMA_VERSION_V1) {
989 return Err(protocol_error(
990 SolveErrorCodeV1::UnsupportedSchemaVersion,
991 format!(
992 "unsupported schema_version {version}; expected {SOLVE_JSON_SCHEMA_VERSION_V1}"
993 ),
994 "$.schema_version",
995 ));
996 }
997 Ok(())
998}
999
1000fn validate_projectile(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1001 let path = "$.projectile";
1002 let object = require_object(value, path)?;
1003 validate_members(
1004 object,
1005 path,
1006 &[
1007 "mass_kg",
1008 "diameter_m",
1009 "length_m",
1010 "drag_model",
1011 "ballistic_coefficient",
1012 ],
1013 &[
1014 "mass_kg",
1015 "diameter_m",
1016 "drag_model",
1017 "ballistic_coefficient",
1018 ],
1019 )?;
1020 validate_required_numbers(
1021 object,
1022 path,
1023 &["mass_kg", "diameter_m", "ballistic_coefficient"],
1024 )?;
1025 validate_optional_number(object, path, "length_m")?;
1026 validate_string_enum(
1027 required_value(object, "drag_model", path)?,
1028 "$.projectile.drag_model",
1029 &["G1", "G6", "G7", "G8"],
1030 )
1031}
1032
1033fn validate_rifle(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1034 let path = "$.rifle";
1035 let object = require_object(value, path)?;
1036 validate_members(
1037 object,
1038 path,
1039 &[
1040 "muzzle_velocity_mps",
1041 "sight_height_m",
1042 "muzzle_height_m",
1043 "twist_rate_m_per_turn",
1044 "twist_direction",
1045 ],
1046 &["muzzle_velocity_mps"],
1047 )?;
1048 validate_required_numbers(object, path, &["muzzle_velocity_mps"])?;
1049 validate_optional_numbers(
1050 object,
1051 path,
1052 &["sight_height_m", "muzzle_height_m", "twist_rate_m_per_turn"],
1053 )?;
1054 if let Some(direction) = object.get("twist_direction") {
1055 validate_string_enum(direction, "$.rifle.twist_direction", &["left", "right"])?;
1056 }
1057 Ok(())
1058}
1059
1060fn validate_shot(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1061 let path = "$.shot";
1062 let object = require_object(value, path)?;
1063 validate_members(
1064 object,
1065 path,
1066 &[
1067 "max_range_m",
1068 "zero_distance_m",
1069 "muzzle_angle_rad",
1070 "aim_azimuth_rad",
1071 "shot_azimuth_rad",
1072 "shooting_angle_rad",
1073 "cant_angle_rad",
1074 "target_height_m",
1075 "ground_threshold_m",
1076 ],
1077 &["max_range_m"],
1078 )?;
1079 validate_required_numbers(object, path, &["max_range_m"])?;
1080 validate_optional_number(object, path, "zero_distance_m")?;
1081 validate_optional_number(object, path, "muzzle_angle_rad")?;
1082 validate_optional_numbers(
1083 object,
1084 path,
1085 &[
1086 "aim_azimuth_rad",
1087 "shot_azimuth_rad",
1088 "shooting_angle_rad",
1089 "cant_angle_rad",
1090 "target_height_m",
1091 "ground_threshold_m",
1092 ],
1093 )
1094}
1095
1096fn validate_atmosphere(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1097 let path = "$.atmosphere";
1098 let object = require_object(value, path)?;
1099 validate_members(
1100 object,
1101 path,
1102 &[
1103 "altitude_m",
1104 "temperature_k",
1105 "pressure_pa",
1106 "relative_humidity",
1107 "latitude_rad",
1108 ],
1109 &[],
1110 )?;
1111 validate_optional_numbers(
1112 object,
1113 path,
1114 &[
1115 "altitude_m",
1116 "temperature_k",
1117 "pressure_pa",
1118 "relative_humidity",
1119 "latitude_rad",
1120 ],
1121 )
1122}
1123
1124fn validate_wind(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1125 let path = "$.wind";
1126 let object = require_object(value, path)?;
1127 validate_members(
1128 object,
1129 path,
1130 &[
1131 "speed_mps",
1132 "direction_from_rad",
1133 "vertical_speed_mps",
1134 "segments",
1135 ],
1136 &[],
1137 )?;
1138 for field in ["speed_mps", "direction_from_rad", "vertical_speed_mps"] {
1139 validate_optional_number(object, path, field)?;
1140 }
1141
1142 if let Some(segments) = object.get("segments") {
1143 let Some(segments) = segments.as_array() else {
1144 return Err(protocol_error(
1145 SolveErrorCodeV1::InvalidValue,
1146 "segments must be an array",
1147 "$.wind.segments",
1148 ));
1149 };
1150 for (index, segment) in segments.iter().enumerate() {
1151 let segment_path = format!("$.wind.segments[{index}]");
1152 let segment = require_object(segment, &segment_path)?;
1153 validate_members(
1154 segment,
1155 &segment_path,
1156 &[
1157 "until_distance_m",
1158 "speed_mps",
1159 "direction_from_rad",
1160 "vertical_speed_mps",
1161 ],
1162 &["until_distance_m", "speed_mps", "direction_from_rad"],
1163 )?;
1164 validate_required_numbers(
1165 segment,
1166 &segment_path,
1167 &["until_distance_m", "speed_mps", "direction_from_rad"],
1168 )?;
1169 validate_optional_number(segment, &segment_path, "vertical_speed_mps")?;
1170 }
1171 }
1172 Ok(())
1173}
1174
1175fn validate_solver(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1176 let path = "$.solver";
1177 let object = require_object(value, path)?;
1178 validate_members(object, path, &["method", "time_step_s"], &[])?;
1179 validate_optional_number(object, path, "time_step_s")?;
1180 if let Some(method) = object.get("method") {
1181 validate_string_enum(method, "$.solver.method", &["euler", "rk4", "rk45"])?;
1182 }
1183 Ok(())
1184}
1185
1186fn validate_effects(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1187 let path = "$.effects";
1188 let object = require_object(value, path)?;
1189 validate_members(
1190 object,
1191 path,
1192 &["magnus", "coriolis", "enhanced_spin_drift"],
1193 &[],
1194 )?;
1195 validate_optional_booleans(object, path, &["magnus", "coriolis", "enhanced_spin_drift"])?;
1196
1197 if object.get("magnus").and_then(Value::as_bool) == Some(true)
1198 && object.get("enhanced_spin_drift").and_then(Value::as_bool) == Some(true)
1199 {
1200 return Err(protocol_error(
1201 SolveErrorCodeV1::ConflictingFields,
1202 "magnus and enhanced_spin_drift cannot both be enabled",
1203 "$.effects",
1204 ));
1205 }
1206
1207 Ok(())
1208}
1209
1210fn validate_sampling(value: &Value) -> Result<(), SolveErrorEnvelopeV1> {
1211 let path = "$.sampling";
1212 let object = require_object(value, path)?;
1213 validate_members(object, path, &["interval_m"], &[])?;
1214 validate_optional_number(object, path, "interval_m")
1215}
1216
1217fn require_object<'a>(
1218 value: &'a Value,
1219 path: &str,
1220) -> Result<&'a Map<String, Value>, SolveErrorEnvelopeV1> {
1221 value
1222 .as_object()
1223 .ok_or_else(|| protocol_error(SolveErrorCodeV1::InvalidValue, "expected an object", path))
1224}
1225
1226fn required_value<'a>(
1227 object: &'a Map<String, Value>,
1228 field: &str,
1229 parent_path: &str,
1230) -> Result<&'a Value, SolveErrorEnvelopeV1> {
1231 object.get(field).ok_or_else(|| {
1232 protocol_error(
1233 SolveErrorCodeV1::MissingField,
1234 format!("missing required field `{field}`"),
1235 child_path(parent_path, field),
1236 )
1237 })
1238}
1239
1240fn validate_members(
1241 object: &Map<String, Value>,
1242 path: &str,
1243 allowed: &[&str],
1244 required: &[&str],
1245) -> Result<(), SolveErrorEnvelopeV1> {
1246 if let Some(field) = object
1247 .keys()
1248 .find(|field| !allowed.contains(&field.as_str()))
1249 {
1250 return Err(protocol_error(
1251 SolveErrorCodeV1::UnknownField,
1252 format!("unknown field `{field}`"),
1253 child_path(path, field),
1254 ));
1255 }
1256
1257 if let Some(field) = required.iter().find(|field| !object.contains_key(**field)) {
1258 return Err(protocol_error(
1259 SolveErrorCodeV1::MissingField,
1260 format!("missing required field `{field}`"),
1261 child_path(path, field),
1262 ));
1263 }
1264 Ok(())
1265}
1266
1267fn validate_string_enum(
1268 value: &Value,
1269 path: &str,
1270 allowed: &[&str],
1271) -> Result<(), SolveErrorEnvelopeV1> {
1272 let Some(value) = value.as_str() else {
1273 return Err(protocol_error(
1274 SolveErrorCodeV1::InvalidValue,
1275 "expected a string enum value",
1276 path,
1277 ));
1278 };
1279 if !allowed.contains(&value) {
1280 return Err(protocol_error(
1281 SolveErrorCodeV1::InvalidValue,
1282 format!(
1283 "invalid value `{value}`; expected one of {}",
1284 allowed.join(", ")
1285 ),
1286 path,
1287 ));
1288 }
1289 Ok(())
1290}
1291
1292fn validate_required_numbers(
1293 object: &Map<String, Value>,
1294 parent_path: &str,
1295 fields: &[&str],
1296) -> Result<(), SolveErrorEnvelopeV1> {
1297 for field in fields {
1298 let value = required_value(object, field, parent_path)?;
1299 validate_number(value, &child_path(parent_path, field))?;
1300 }
1301 Ok(())
1302}
1303
1304fn validate_optional_numbers(
1305 object: &Map<String, Value>,
1306 parent_path: &str,
1307 fields: &[&str],
1308) -> Result<(), SolveErrorEnvelopeV1> {
1309 for field in fields {
1310 validate_optional_number(object, parent_path, field)?;
1311 }
1312 Ok(())
1313}
1314
1315fn validate_optional_number(
1316 object: &Map<String, Value>,
1317 parent_path: &str,
1318 field: &str,
1319) -> Result<(), SolveErrorEnvelopeV1> {
1320 if let Some(value) = object.get(field) {
1321 validate_number(value, &child_path(parent_path, field))?;
1322 }
1323 Ok(())
1324}
1325
1326fn validate_number(value: &Value, path: &str) -> Result<(), SolveErrorEnvelopeV1> {
1327 if value.is_number() {
1328 Ok(())
1329 } else {
1330 Err(protocol_error(
1331 SolveErrorCodeV1::InvalidValue,
1332 "expected a number",
1333 path,
1334 ))
1335 }
1336}
1337
1338fn validate_optional_booleans(
1339 object: &Map<String, Value>,
1340 parent_path: &str,
1341 fields: &[&str],
1342) -> Result<(), SolveErrorEnvelopeV1> {
1343 for field in fields {
1344 if let Some(value) = object.get(*field) {
1345 if !value.is_boolean() {
1346 return Err(protocol_error(
1347 SolveErrorCodeV1::InvalidValue,
1348 "expected a boolean",
1349 child_path(parent_path, field),
1350 ));
1351 }
1352 }
1353 }
1354 Ok(())
1355}
1356
1357fn child_path(parent: &str, field: &str) -> String {
1358 format!("{parent}.{field}")
1359}