github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/api/reqsimport/reqsimport.go (about)

     1  package reqsimport
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"path"
     9  	"time"
    10  
    11  	"github.com/ActiveState/cli/internal/language"
    12  	"github.com/ActiveState/cli/internal/locale"
    13  	"github.com/ActiveState/cli/internal/logging"
    14  	"github.com/ActiveState/cli/pkg/platform/api"
    15  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_models"
    16  )
    17  
    18  const (
    19  	jsonContentType = "application/json"
    20  )
    21  
    22  // Opts contains the options available for the primary package type ReqsImport.
    23  type Opts struct {
    24  	ReqsvcURL string
    25  }
    26  
    27  // ReqsImport represents a reusable http.Client and related options.
    28  type ReqsImport struct {
    29  	opts   Opts
    30  	client *http.Client
    31  }
    32  
    33  // New forms a pointer to a default ReqsImport instance.
    34  func New(opts Opts) (*ReqsImport, error) {
    35  	c := &http.Client{
    36  		Timeout:   60 * time.Second,
    37  		Transport: api.NewRoundTripper(http.DefaultTransport),
    38  	}
    39  
    40  	ri := ReqsImport{
    41  		opts:   opts,
    42  		client: c,
    43  	}
    44  
    45  	return &ri, nil
    46  }
    47  
    48  // Init is a convenience wrapper for New.
    49  func Init() *ReqsImport {
    50  	svcURL := api.GetServiceURL(api.ServiceRequirementsImport)
    51  	url := svcURL.Scheme + "://" + path.Join(svcURL.Host, svcURL.Path)
    52  
    53  	opts := Opts{
    54  		ReqsvcURL: url,
    55  	}
    56  
    57  	ri, err := New(opts)
    58  	if err != nil {
    59  		panic(err)
    60  	}
    61  
    62  	return ri
    63  }
    64  
    65  // Changeset posts requirements data to a backend service and returns a
    66  // Changeset that can be committed to a project.
    67  func (ri *ReqsImport) Changeset(data []byte, lang string) ([]*mono_models.CommitChangeEditable, error) {
    68  	reqPayload := &TranslationReqMsg{
    69  		Data:     string(data),
    70  		Language: lang,
    71  	}
    72  	if lang == "" {
    73  		// The endpoint requires a valid language name. It is not present in the requirements read and
    74  		// returned. When coupled with "unformatted=true", the language has no bearing on the
    75  		// translation, so just pick one.
    76  		reqPayload.Language = language.Python3.Requirement()
    77  		reqPayload.Unformatted = true
    78  	}
    79  	respPayload := &TranslationRespMsg{}
    80  
    81  	err := postJSON(ri.client, ri.opts.ReqsvcURL, reqPayload, respPayload)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	if len(respPayload.LineErrs) > 0 {
    87  		return nil, &TranslationResponseError{respPayload.LineErrs}
    88  	}
    89  
    90  	return respPayload.Changeset, nil
    91  }
    92  
    93  // TranslationReqMsg represents the message sent to the requirements
    94  // translation service.
    95  type TranslationReqMsg struct {
    96  	Data        string `json:"requirements"`
    97  	Language    string `json:"language"`
    98  	Unformatted bool   `json:"unformatted"`
    99  }
   100  
   101  // TranslationRespMsg represents the message returned by the requirements
   102  // translation service.
   103  type TranslationRespMsg struct {
   104  	Changeset []*mono_models.CommitChangeEditable `json:"changeset,omitempty"`
   105  	LineErrs  []TranslationLineError              `json:"errors,omitempty"`
   106  }
   107  
   108  // TranslationLineError represents an error reported by the requirements
   109  // translation service regarding a single line processed from the request.
   110  type TranslationLineError struct {
   111  	ErrMsg string `json:"errorText,omitempty"`
   112  	PkgTxt string `json:"packageText,omitempty"`
   113  }
   114  
   115  // Error implements the error interface.
   116  func (e *TranslationLineError) Error() string {
   117  	return fmt.Sprintf("line %q: %s", e.PkgTxt, e.ErrMsg)
   118  }
   119  
   120  // TranslationResponseError contains multiple error messages and allows them to
   121  // be handled as a common error.
   122  type TranslationResponseError struct {
   123  	LineErrs []TranslationLineError
   124  }
   125  
   126  // Error implements the error interface.
   127  func (e *TranslationResponseError) Error() string {
   128  	var msgs, sep string
   129  	for _, lineErr := range e.LineErrs {
   130  		msgs += sep + lineErr.Error()
   131  		sep = "; "
   132  	}
   133  	if msgs == "" {
   134  		msgs = "unknown error"
   135  	}
   136  
   137  	return locale.Tr("reqsvc_err_line_errors", msgs)
   138  }
   139  
   140  func postJSON(client *http.Client, url string, reqPayload, respPayload interface{}) error {
   141  	var buf bytes.Buffer
   142  	if err := json.NewEncoder(&buf).Encode(reqPayload); err != nil {
   143  		return err
   144  	}
   145  
   146  	logging.Debug("POSTing JSON")
   147  	resp, err := client.Post(url, jsonContentType, &buf)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	defer resp.Body.Close() //nolint
   152  
   153  	return json.NewDecoder(resp.Body).Decode(&respPayload)
   154  }