@@ -218,8 +218,17 @@ impl ServiceEndpoints {
218218/// Runtime outputs generated during deployment operations
219219///
220220/// This struct contains fields that are generated during deployment operations
221- /// and represent the runtime state of deployed infrastructure. These fields
222- /// are mutable as operations progress.
221+ /// and represent the runtime state of deployed infrastructure. Fields are
222+ /// private to protect invariants and provide semantic clarity through setters.
223+ ///
224+ /// # Lifecycle
225+ ///
226+ /// Fields are populated at different stages of the deployment lifecycle:
227+ /// - **Creation**: All fields are `None` (use `RuntimeOutputs::new()`)
228+ /// - **After Provisioning**: `instance_ip` and `provision_method` are set
229+ /// (use `record_provisioning()` or `record_registration()`)
230+ /// - **After Run Command**: `service_endpoints` is set
231+ /// (use `record_services_started()`)
223232///
224233/// # Future Fields
225234///
@@ -233,19 +242,23 @@ impl ServiceEndpoints {
233242/// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::{RuntimeOutputs, ProvisionMethod};
234243/// use std::net::{IpAddr, Ipv4Addr};
235244///
236- /// let runtime_outputs = RuntimeOutputs {
237- /// instance_ip: Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))),
238- /// provision_method: Some(ProvisionMethod::Provisioned),
239- /// service_endpoints: None,
240- /// };
245+ /// // Create empty runtime outputs
246+ /// let mut runtime_outputs = RuntimeOutputs::new();
247+ /// assert!(runtime_outputs.instance_ip().is_none());
248+ ///
249+ /// // After provisioning, record the IP and method
250+ /// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100));
251+ /// runtime_outputs.record_provisioning(ip);
252+ /// assert_eq!(runtime_outputs.instance_ip(), Some(ip));
253+ /// assert_eq!(runtime_outputs.provision_method(), Some(ProvisionMethod::Provisioned));
241254/// ```
242255#[ derive( Debug , Clone , Serialize , Deserialize ) ]
243256pub struct RuntimeOutputs {
244257 /// Instance IP address (populated after provisioning)
245258 ///
246259 /// This field stores the IP address of the provisioned instance and is
247260 /// `None` until the environment has been successfully provisioned.
248- pub instance_ip : Option < IpAddr > ,
261+ instance_ip : Option < IpAddr > ,
249262
250263 /// How the instance was provisioned
251264 ///
@@ -257,7 +270,7 @@ pub struct RuntimeOutputs {
257270 /// - `Some(Provisioned)`: Instance was created via `provision` command
258271 /// - `Some(Registered)`: Instance was connected via `register` command
259272 #[ serde( default ) ]
260- pub provision_method : Option < ProvisionMethod > ,
273+ provision_method : Option < ProvisionMethod > ,
261274
262275 /// Service endpoints populated after services are started
263276 ///
@@ -267,5 +280,130 @@ pub struct RuntimeOutputs {
267280 /// - `None`: Services not yet started or legacy state
268281 /// - `Some(endpoints)`: URLs for all running services
269282 #[ serde( default ) ]
270- pub service_endpoints : Option < ServiceEndpoints > ,
283+ service_endpoints : Option < ServiceEndpoints > ,
284+ }
285+
286+ impl RuntimeOutputs {
287+ /// Creates new empty runtime outputs
288+ ///
289+ /// All fields are initialized to `None`, representing an environment
290+ /// that has not yet been provisioned or run.
291+ ///
292+ /// # Examples
293+ ///
294+ /// ```rust
295+ /// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::RuntimeOutputs;
296+ ///
297+ /// let outputs = RuntimeOutputs::new();
298+ /// assert!(outputs.instance_ip().is_none());
299+ /// assert!(outputs.provision_method().is_none());
300+ /// assert!(outputs.service_endpoints().is_none());
301+ /// ```
302+ #[ must_use]
303+ pub fn new ( ) -> Self {
304+ Self {
305+ instance_ip : None ,
306+ provision_method : None ,
307+ service_endpoints : None ,
308+ }
309+ }
310+
311+ // =========================================================================
312+ // Getters - Access runtime output values
313+ // =========================================================================
314+
315+ /// Returns the instance IP address if available
316+ ///
317+ /// This is `None` until the environment has been provisioned or registered.
318+ #[ must_use]
319+ pub fn instance_ip ( & self ) -> Option < IpAddr > {
320+ self . instance_ip
321+ }
322+
323+ /// Returns how the instance was provisioned
324+ ///
325+ /// - `None`: Unknown or legacy state
326+ /// - `Some(Provisioned)`: Created via `provision` command
327+ /// - `Some(Registered)`: Connected via `register` command
328+ #[ must_use]
329+ pub fn provision_method ( & self ) -> Option < ProvisionMethod > {
330+ self . provision_method
331+ }
332+
333+ /// Returns the service endpoints if available
334+ ///
335+ /// This is `None` until the `run` command has started services successfully.
336+ #[ must_use]
337+ pub fn service_endpoints ( & self ) -> Option < & ServiceEndpoints > {
338+ self . service_endpoints . as_ref ( )
339+ }
340+
341+ // =========================================================================
342+ // Semantic Setters - Record deployment lifecycle events
343+ // =========================================================================
344+
345+ /// Records that provisioning has completed with the given instance IP
346+ ///
347+ /// Call this after the `provision` command successfully creates infrastructure.
348+ /// Sets both `instance_ip` and `provision_method` to `Provisioned`.
349+ ///
350+ /// # Arguments
351+ ///
352+ /// * `ip` - The IP address of the newly provisioned instance
353+ pub fn record_provisioning ( & mut self , ip : IpAddr ) {
354+ self . instance_ip = Some ( ip) ;
355+ self . provision_method = Some ( ProvisionMethod :: Provisioned ) ;
356+ }
357+
358+ /// Records that an existing instance has been registered
359+ ///
360+ /// Call this after the `register` command connects to existing infrastructure.
361+ /// Sets both `instance_ip` and `provision_method` to `Registered`.
362+ ///
363+ /// # Arguments
364+ ///
365+ /// * `ip` - The IP address of the registered instance
366+ pub fn record_registration ( & mut self , ip : IpAddr ) {
367+ self . instance_ip = Some ( ip) ;
368+ self . provision_method = Some ( ProvisionMethod :: Registered ) ;
369+ }
370+
371+ /// Records that services have been started with the given endpoints
372+ ///
373+ /// Call this after the `run` command successfully starts all services.
374+ /// The endpoints can then be displayed to users or used for health checks.
375+ ///
376+ /// # Arguments
377+ ///
378+ /// * `endpoints` - The URLs for all running services
379+ pub fn record_services_started ( & mut self , endpoints : ServiceEndpoints ) {
380+ self . service_endpoints = Some ( endpoints) ;
381+ }
382+
383+ // =========================================================================
384+ // Low-level setters - For backward compatibility and state restoration
385+ // =========================================================================
386+
387+ /// Sets the instance IP directly
388+ ///
389+ /// Prefer `record_provisioning()` or `record_registration()` which also
390+ /// set the provision method. This method is provided for cases where
391+ /// only the IP needs to be updated (e.g., deserialization workarounds).
392+ pub fn set_instance_ip ( & mut self , ip : IpAddr ) {
393+ self . instance_ip = Some ( ip) ;
394+ }
395+
396+ /// Sets the provision method directly
397+ ///
398+ /// Prefer `record_provisioning()` or `record_registration()` which also
399+ /// set the instance IP. This method is provided for backward compatibility.
400+ pub fn set_provision_method ( & mut self , method : ProvisionMethod ) {
401+ self . provision_method = Some ( method) ;
402+ }
403+ }
404+
405+ impl Default for RuntimeOutputs {
406+ fn default ( ) -> Self {
407+ Self :: new ( )
408+ }
271409}
0 commit comments