1+ import json
12from typing import Optional , Tuple , List , Union , Self , ClassVar , Literal
3+
4+ from pydantic import BaseModel , Field , field_validator
25from src .models .interface import ApiBaseModel
36from src .models .motor import MotorModel
47from src .models .sub .aerosurfaces import (
1013)
1114
1215
16+ def _maybe_parse_json (value ):
17+ if isinstance (value , str ):
18+ try :
19+ return json .loads (value )
20+ except json .JSONDecodeError as exc :
21+ raise ValueError ('Invalid JSON payload' ) from exc
22+ return value
23+
24+
1325class RocketModel (ApiBaseModel ):
1426 NAME : ClassVar = "rocket"
1527 METHODS : ClassVar = ("POST" , "GET" , "PUT" , "DELETE" )
@@ -37,6 +49,42 @@ class RocketModel(ApiBaseModel):
3749 rail_buttons : Optional [RailButtons ] = None
3850 tail : Optional [Tail ] = None
3951
52+ @field_validator ('motor' , mode = 'before' )
53+ @classmethod
54+ def _coerce_motor (cls , value ):
55+ return _maybe_parse_json (value )
56+
57+ @field_validator ('nose' , mode = 'before' )
58+ @classmethod
59+ def _coerce_nose (cls , value ):
60+ return _maybe_parse_json (value )
61+
62+ @field_validator ('fins' , mode = 'before' )
63+ @classmethod
64+ def _coerce_fins (cls , value ):
65+ value = _maybe_parse_json (value )
66+ if isinstance (value , dict ):
67+ value = [value ]
68+ return value
69+
70+ @field_validator ('parachutes' , mode = 'before' )
71+ @classmethod
72+ def _coerce_parachutes (cls , value ):
73+ value = _maybe_parse_json (value )
74+ if isinstance (value , dict ):
75+ value = [value ]
76+ return value
77+
78+ @field_validator ('rail_buttons' , mode = 'before' )
79+ @classmethod
80+ def _coerce_rail_buttons (cls , value ):
81+ return _maybe_parse_json (value )
82+
83+ @field_validator ('tail' , mode = 'before' )
84+ @classmethod
85+ def _coerce_tail (cls , value ):
86+ return _maybe_parse_json (value )
87+
4088 @staticmethod
4189 def UPDATED ():
4290 return
@@ -61,3 +109,53 @@ def RETRIEVED(model_instance: type(Self)):
61109 ** model_instance .model_dump (),
62110 )
63111 )
112+
113+
114+ class RocketPartialModel (BaseModel ):
115+ """Rocket attributes required when a motor is supplied by reference."""
116+
117+ radius : float
118+ mass : float
119+ motor_position : float
120+ center_of_mass_without_motor : float
121+ inertia : Union [
122+ Tuple [float , float , float ],
123+ Tuple [float , float , float , float , float , float ],
124+ ] = (0 , 0 , 0 )
125+ power_off_drag : List [Tuple [float , float ]] = Field (
126+ default_factory = lambda : [(0 , 0 )]
127+ )
128+ power_on_drag : List [Tuple [float , float ]] = Field (
129+ default_factory = lambda : [(0 , 0 )]
130+ )
131+ coordinate_system_orientation : Literal ['tail_to_nose' , 'nose_to_tail' ] = (
132+ 'tail_to_nose'
133+ )
134+ nose : NoseCone
135+ fins : List [Fins ]
136+ parachutes : Optional [List [Parachute ]] = None
137+ rail_buttons : Optional [RailButtons ] = None
138+ tail : Optional [Tail ] = None
139+
140+ def assemble (self , motor : MotorModel ) -> RocketModel :
141+ """Compose a full rocket model using the referenced motor."""
142+
143+ rocket_data = self .model_dump (exclude_none = True )
144+ return RocketModel (motor = motor , ** rocket_data )
145+
146+
147+ class RocketWithMotorReferenceRequest (BaseModel ):
148+ """Payload for creating or updating rockets via motor reference."""
149+
150+ motor_id : str
151+ rocket : RocketPartialModel
152+
153+ @field_validator ('rocket' , mode = 'before' )
154+ @classmethod
155+ def _coerce_rocket (cls , value ):
156+ if isinstance (value , str ):
157+ try :
158+ value = json .loads (value )
159+ except json .JSONDecodeError as exc :
160+ raise ValueError ('Invalid JSON for rocket payload' ) from exc
161+ return value
0 commit comments