github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/internal/ota-api/dto.go (about)

     1  // This file is part of arduino-cloud-cli.
     2  //
     3  // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/)
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published
     7  // by the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    17  
    18  package otaapi
    19  
    20  import (
    21  	"strings"
    22  	"time"
    23  
    24  	"unicode"
    25  
    26  	"github.com/arduino/arduino-cli/table"
    27  )
    28  
    29  type (
    30  	OtaStatusResponse struct {
    31  		Ota    Ota     `json:"ota"`
    32  		States []State `json:"states,omitempty"`
    33  	}
    34  
    35  	OtaStatusList struct {
    36  		Ota []Ota `json:"ota"`
    37  	}
    38  
    39  	Ota struct {
    40  		ID          string `json:"id,omitempty" yaml:"id,omitempty"`
    41  		DeviceID    string `json:"device_id,omitempty" yaml:"device_id,omitempty"`
    42  		Status      string `json:"status" yaml:"status"`
    43  		StartedAt   string `json:"started_at" yaml:"started_at"`
    44  		EndedAt     string `json:"ended_at,omitempty" yaml:"ended_at,omitempty"`
    45  		ErrorReason string `json:"error_reason,omitempty" yaml:"error_reason,omitempty"`
    46  	}
    47  
    48  	State struct {
    49  		OtaID     string `json:"ota_id"`
    50  		State     string `json:"state"`
    51  		StateData string `json:"state_data,omitempty"`
    52  		Timestamp string `json:"timestamp,omitempty"`
    53  	}
    54  
    55  	OtaStatusDetail struct {
    56  		Ota     Ota     `json:"ota"`
    57  		Details []State `json:"details,omitempty"`
    58  	}
    59  )
    60  
    61  func (r OtaStatusList) Data() interface{} {
    62  	return r.Ota
    63  }
    64  
    65  func (r OtaStatusList) String() string {
    66  	if len(r.Ota) == 0 {
    67  		return ""
    68  	}
    69  	t := table.New()
    70  	hasErrorReason := false
    71  	for _, r := range r.Ota {
    72  		if r.ErrorReason != "" {
    73  			hasErrorReason = true
    74  			break
    75  		}
    76  	}
    77  
    78  	if hasErrorReason {
    79  		t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At", "Error Reason")
    80  	} else {
    81  		t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At")
    82  	}
    83  
    84  	// Now print the table
    85  	for _, r := range r.Ota {
    86  		line := []any{r.DeviceID, r.ID, r.MapStatus(), formatHumanReadableTs(r.StartedAt), formatHumanReadableTs(r.EndedAt)}
    87  		if hasErrorReason {
    88  			line = append(line, r.ErrorReason)
    89  		}
    90  		t.AddRow(line...)
    91  	}
    92  
    93  	return t.Render()
    94  }
    95  
    96  func (o Ota) MapStatus() string {
    97  	return upperCaseFirst(o.Status)
    98  }
    99  
   100  func (r Ota) Data() interface{} {
   101  	return r
   102  }
   103  
   104  func (r Ota) String() string {
   105  	if len(r.ID) == 0 {
   106  		return ""
   107  	}
   108  	t := table.New()
   109  	hasErrorReason := r.ErrorReason != ""
   110  
   111  	if hasErrorReason {
   112  		t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At", "Error Reason")
   113  	} else {
   114  		t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At")
   115  	}
   116  
   117  	// Now print the table
   118  	line := []any{r.DeviceID, r.ID, r.MapStatus(), formatHumanReadableTs(r.StartedAt), formatHumanReadableTs(r.EndedAt)}
   119  	if hasErrorReason {
   120  		line = append(line, r.ErrorReason)
   121  	}
   122  	t.AddRow(line...)
   123  
   124  	return t.Render()
   125  }
   126  
   127  func (r OtaStatusDetail) Data() interface{} {
   128  	return r.Ota
   129  }
   130  
   131  func (r OtaStatusDetail) String() string {
   132  	if r.Ota.ID == "" {
   133  		return "No OTA found"
   134  	}
   135  	t := table.New()
   136  	hasErrorReason := r.Ota.ErrorReason != ""
   137  
   138  	if hasErrorReason {
   139  		t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At", "Error Reason")
   140  	} else {
   141  		t.SetHeader("Device ID", "Ota ID", "Status", "Started At", "Ended At")
   142  	}
   143  
   144  	// Now print the table
   145  	line := []any{r.Ota.DeviceID, r.Ota.ID, r.Ota.MapStatus(), formatHumanReadableTs(r.Ota.StartedAt), formatHumanReadableTs(r.Ota.EndedAt)}
   146  	if hasErrorReason {
   147  		line = append(line, r.Ota.ErrorReason)
   148  	}
   149  	t.AddRow(line...)
   150  
   151  	output := t.Render()
   152  
   153  	// Add details
   154  	if len(r.Details) > 0 {
   155  		t = table.New()
   156  		t.SetHeader("Time", "Status", "Detail")
   157  		for _, s := range r.Details {
   158  			t.AddRow(formatHumanReadableTs(s.Timestamp), upperCaseFirst(s.State), s.StateData)
   159  		}
   160  		output += "\nDetails:\n" + t.Render()
   161  	}
   162  
   163  	return output
   164  }
   165  
   166  func upperCaseFirst(s string) string {
   167  	if len(s) > 0 {
   168  		s = strings.ReplaceAll(s, "_", " ")
   169  		for i, v := range s {
   170  			return string(unicode.ToUpper(v)) + s[i+1:]
   171  		}
   172  	}
   173  	return ""
   174  }
   175  
   176  func formatHumanReadableTs(ts string) string {
   177  	if ts == "" {
   178  		return ""
   179  	}
   180  	parsed, err := time.Parse(time.RFC3339Nano, ts)
   181  	if err != nil {
   182  		return ts
   183  	}
   184  	return parsed.Format(time.RFC3339)
   185  }