11namespace VastAI . NET . Serialization ;
22
3+ using System . Globalization ;
4+ using System . Text . Json ;
35using System . Text . Json . Serialization ;
46using Models ;
57using static VastResponseHelpers ;
@@ -38,9 +40,11 @@ internal sealed class VastInstanceDto
3840 public double DirectSshPort { get ; set ; }
3941 public string SshUser { get ; set ; } = "" ;
4042 public string User { get ; set ; } = "" ;
43+ public JsonElement ExtraEnv { get ; set ; }
44+ public JsonElement Ports { get ; set ; }
4145
4246 /// <summary>Maps raw Vast instance fields to the normalized model used by safety and SSH discovery.</summary>
43- public VastInstance ToModel ( )
47+ public VastInstance ToModel ( VastRawJson ? raw = null )
4448 {
4549 var ageHours = FirstPositive ( AgeHours , AgeHrs , DurationHours ) ;
4650 var start = FirstPositive ( StartDate , CreatedAt , CreationDate , RentedAt ) ;
@@ -56,6 +60,167 @@ public VastInstance ToModel()
5660 ageHours ,
5761 FirstNonEmpty ( SshHost , DirectSshHost , PublicIpaddr ) ,
5862 ( int ) FirstPositive ( SshPort , DirectSshPort ) ,
59- FirstNonEmpty ( SshUser , User , "root" ) ) ;
63+ FirstNonEmpty ( SshUser , User , "root" ) ,
64+ PublicIpaddr ,
65+ ReadExtraEnvironment ( ExtraEnv ) ,
66+ ReadPorts ( Ports ) ,
67+ raw ) ;
68+ }
69+
70+ /// <summary>Normalizes Vast extra_env when it arrives as arrays or objects.</summary>
71+ private static IReadOnlyDictionary < string , string > ReadExtraEnvironment ( JsonElement extraEnv )
72+ {
73+ var values = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
74+ if ( extraEnv . ValueKind is JsonValueKind . Undefined or JsonValueKind . Null )
75+ return values ;
76+
77+ if ( extraEnv . ValueKind == JsonValueKind . Object )
78+ {
79+ foreach ( var property in extraEnv . EnumerateObject ( ) )
80+ values [ property . Name ] = property . Value . ToString ( ) ;
81+ return values ;
82+ }
83+
84+ if ( extraEnv . ValueKind != JsonValueKind . Array )
85+ return values ;
86+
87+ foreach ( var item in extraEnv . EnumerateArray ( ) )
88+ if ( TryReadExtraEnvironmentItem ( item , out var name , out var value ) )
89+ values [ name ] = value ;
90+
91+ return values ;
92+ }
93+
94+ /// <summary>Reads one Vast extra_env item from the documented array shape or an object alias shape.</summary>
95+ private static bool TryReadExtraEnvironmentItem ( JsonElement item , out string name , out string value )
96+ {
97+ name = "" ;
98+ value = "" ;
99+ if ( item . ValueKind == JsonValueKind . Array && item . GetArrayLength ( ) >= 2 )
100+ {
101+ name = item [ 0 ] . ToString ( ) ;
102+ value = item [ 1 ] . ToString ( ) ;
103+ return ! string . IsNullOrWhiteSpace ( name ) ;
104+ }
105+
106+ if ( item . ValueKind != JsonValueKind . Object )
107+ return false ;
108+
109+ name = ReadStringProperty ( item , "name" , "key" ) ;
110+ value = ReadStringProperty ( item , "value" ) ;
111+ return ! string . IsNullOrWhiteSpace ( name ) ;
112+ }
113+
114+ /// <summary>Normalizes Vast ports when they arrive as Docker-style objects or arrays of mapping objects.</summary>
115+ private static IReadOnlyList < VastPortMapping > ReadPorts ( JsonElement ports )
116+ {
117+ if ( ports . ValueKind is JsonValueKind . Undefined or JsonValueKind . Null )
118+ return [ ] ;
119+
120+ if ( ports . ValueKind == JsonValueKind . Array )
121+ return ports . EnumerateArray ( ) . Select ( ReadArrayPortMapping ) . Where ( mapping => mapping is not null ) . Cast < VastPortMapping > ( ) . ToList ( ) ;
122+
123+ if ( ports . ValueKind != JsonValueKind . Object )
124+ return [ ] ;
125+
126+ var mappings = new List < VastPortMapping > ( ) ;
127+ foreach ( var property in ports . EnumerateObject ( ) )
128+ {
129+ var ( containerPort , protocol ) = ParsePortKey ( property . Name ) ;
130+ if ( containerPort <= 0 )
131+ continue ;
132+
133+ if ( property . Value . ValueKind == JsonValueKind . Array )
134+ mappings . AddRange ( property . Value . EnumerateArray ( )
135+ . Select ( item => ReadDockerPortMapping ( containerPort , protocol , item ) )
136+ . Where ( mapping => mapping is not null )
137+ . Cast < VastPortMapping > ( ) ) ;
138+ else
139+ {
140+ var mapping = ReadDockerPortMapping ( containerPort , protocol , property . Value ) ;
141+ if ( mapping is not null )
142+ mappings . Add ( mapping ) ;
143+ }
144+ }
145+
146+ return mappings ;
147+ }
148+
149+ /// <summary>Reads one array-style Vast port mapping.</summary>
150+ private static VastPortMapping ? ReadArrayPortMapping ( JsonElement item )
151+ {
152+ if ( item . ValueKind != JsonValueKind . Object )
153+ return null ;
154+
155+ var containerPort = ReadIntProperty ( item , "container_port" , "containerPort" , "port" , "private_port" ) ;
156+ var hostPort = ReadIntProperty ( item , "host_port" , "hostPort" , "public_port" ) ;
157+ if ( containerPort <= 0 || hostPort <= 0 )
158+ return null ;
159+
160+ return new VastPortMapping (
161+ containerPort ,
162+ FirstNonEmpty ( ReadStringProperty ( item , "protocol" , "proto" ) , "tcp" ) ,
163+ ReadStringProperty ( item , "host" , "host_ip" , "HostIp" , "ip" ) ,
164+ hostPort ) ;
165+ }
166+
167+ /// <summary>Reads one Docker-style Vast port mapping value.</summary>
168+ private static VastPortMapping ? ReadDockerPortMapping ( int containerPort , string protocol , JsonElement item )
169+ {
170+ if ( item . ValueKind is JsonValueKind . Number or JsonValueKind . String )
171+ {
172+ var scalarPort = ReadInt ( item ) ;
173+ return scalarPort <= 0 ? null : new VastPortMapping ( containerPort , protocol , "" , scalarPort ) ;
174+ }
175+
176+ if ( item . ValueKind != JsonValueKind . Object )
177+ return null ;
178+
179+ var hostPort = ReadIntProperty ( item , "HostPort" , "host_port" , "hostPort" , "public_port" ) ;
180+ if ( hostPort <= 0 )
181+ return null ;
182+
183+ return new VastPortMapping (
184+ containerPort ,
185+ protocol ,
186+ ReadStringProperty ( item , "HostIp" , "host_ip" , "host" , "ip" ) ,
187+ hostPort ) ;
188+ }
189+
190+ /// <summary>Reads the container port and protocol from a Docker-style key like 8088/tcp.</summary>
191+ private static ( int ContainerPort , string Protocol ) ParsePortKey ( string key )
192+ {
193+ var parts = key . Split ( '/' , 2 , StringSplitOptions . TrimEntries ) ;
194+ return ( int . TryParse ( parts [ 0 ] , NumberStyles . Integer , CultureInfo . InvariantCulture , out var port ) ? port : 0 ,
195+ parts . Length == 2 && ! string . IsNullOrWhiteSpace ( parts [ 1 ] ) ? parts [ 1 ] : "tcp" ) ;
196+ }
197+
198+ /// <summary>Reads the first matching string property from a JSON object.</summary>
199+ private static string ReadStringProperty ( JsonElement item , params string [ ] names )
200+ {
201+ foreach ( var name in names )
202+ if ( item . TryGetProperty ( name , out var property ) )
203+ return property . ToString ( ) ;
204+
205+ return "" ;
206+ }
207+
208+ /// <summary>Reads the first matching integer property from a JSON object.</summary>
209+ private static int ReadIntProperty ( JsonElement item , params string [ ] names )
210+ {
211+ foreach ( var name in names )
212+ if ( item . TryGetProperty ( name , out var property ) )
213+ return ReadInt ( property ) ;
214+
215+ return 0 ;
216+ }
217+
218+ /// <summary>Reads a JSON number or numeric string as an integer.</summary>
219+ private static int ReadInt ( JsonElement value )
220+ {
221+ if ( value . ValueKind == JsonValueKind . Number && value . TryGetInt32 ( out var number ) )
222+ return number ;
223+
224+ return int . TryParse ( value . ToString ( ) , NumberStyles . Integer , CultureInfo . InvariantCulture , out number ) ? number : 0 ;
60225 }
61226}
0 commit comments