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

     1  package request
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"os"
     8  
     9  	"github.com/ActiveState/cli/internal/errs"
    10  	"github.com/ActiveState/cli/internal/fileutils"
    11  	"github.com/ActiveState/cli/internal/gqlclient"
    12  	"github.com/ActiveState/cli/internal/locale"
    13  	yamlcomment "github.com/zijiren233/yaml-comment"
    14  	"gopkg.in/yaml.v3"
    15  )
    16  
    17  const (
    18  	DependencyTypeRuntime = "runtime"
    19  	DependencyTypeBuild   = "build"
    20  	DependencyTypeTest    = "test"
    21  )
    22  
    23  func Publish(vars PublishVariables, filepath string) (*PublishInput, error) {
    24  	var f *os.File
    25  	if filepath != "" {
    26  		var err error
    27  		f, err = os.Open(filepath)
    28  		if err != nil {
    29  			if errors.Is(err, os.ErrNotExist) {
    30  				return nil, locale.WrapExternalError(err, "err_upload_file_not_found", "Could not find file at {{.V0}}", filepath)
    31  			}
    32  			return nil, errs.Wrap(err, "Could not open file %s", filepath)
    33  		}
    34  
    35  		checksum, err := fileutils.Sha256Hash(filepath)
    36  		if err != nil {
    37  			return nil, locale.WrapError(err, "err_upload_file_checksum", "Could not calculate checksum for file")
    38  		}
    39  
    40  		vars.FileChecksum = checksum
    41  	}
    42  
    43  	return &PublishInput{
    44  		Variables: vars,
    45  		file:      f,
    46  	}, nil
    47  }
    48  
    49  // PublishVariables holds the input variables
    50  // It is ultimately used as the input for the graphql query, but before that we may want to present the data to the user
    51  // which is done with yaml. As such the yaml tags are used for representing data to the user, and the json is used for
    52  // inputs to graphql.
    53  type PublishVariables struct {
    54  	Name        string `yaml:"name" json:"-" hc:"The name of the ingredient"`                                                                                                           // User representation only
    55  	Namespace   string `yaml:"namespace" json:"-" hc:"The namespace field should be in the format org/folder. Org can simply be your username or any organization you're a member of."` // User representation only
    56  	Version     string `yaml:"version" json:"version" hc:"The version field should follow semantic versioning and match the version in the filename (if any)."`
    57  	Description string `yaml:"description" json:"description" hc:"The description field should be a short description of the ingredient."`
    58  
    59  	// Optional
    60  	Authors      []PublishVariableAuthor  `yaml:"authors,omitempty" json:"authors,omitempty" hc:"A list of authors who contributed to the ingredient."`
    61  	Dependencies []PublishVariableDep     `yaml:"dependencies,omitempty" json:"dependencies,omitempty" hc:"A list of dependencies that the ingredient requires."`
    62  	Features     []PublishVariableFeature `yaml:"features,omitempty" json:"features,omitempty" hc:"A list of features that the ingredient provides."`
    63  
    64  	// GraphQL input only
    65  	Path         string  `yaml:"-" json:"path"`
    66  	File         *string `yaml:"-" json:"file"` // Intentionally a pointer that never gets set as the server expects this to always be nil
    67  	FileChecksum string  `yaml:"-" json:"file_checksum"`
    68  }
    69  
    70  type PublishVariableAuthor struct {
    71  	Name     string   `yaml:"name,omitempty" json:"name,omitempty"`
    72  	Email    string   `yaml:"email,omitempty" json:"email,omitempty"`
    73  	Websites []string `yaml:"websites,omitempty" json:"websites,omitempty"`
    74  }
    75  
    76  type PublishVariableDep struct {
    77  	Dependency
    78  	Conditions []Dependency `yaml:"conditions,omitempty" json:"conditions,omitempty"`
    79  }
    80  
    81  type PublishVariableFeature struct {
    82  	Name      string `yaml:"name" json:"name"`
    83  	Namespace string `yaml:"namespace" json:"namespace"`
    84  	Version   string `yaml:"version" json:"version"`
    85  }
    86  
    87  type Dependency struct {
    88  	Name                string `yaml:"name" json:"name"`
    89  	Namespace           string `yaml:"namespace" json:"namespace"`
    90  	VersionRequirements string `yaml:"versionRequirements,omitempty" json:"versionRequirements,omitempty"`
    91  	Type                string `yaml:"type,omitempty" json:"type,omitempty"`
    92  }
    93  
    94  // ExampleAuthorVariables is used for presenting sample data to the user, it's not used for graphql input
    95  type ExampleAuthorVariables struct {
    96  	Authors []PublishVariableAuthor `yaml:"authors,omitempty"`
    97  }
    98  
    99  // ExampleDepVariables is used for presenting sample data to the user, it's not used for graphql input
   100  type ExampleDepVariables struct {
   101  	Dependencies []PublishVariableDep `yaml:"dependencies,omitempty"`
   102  }
   103  
   104  func (p PublishVariables) MarshalYaml(includeExample bool) ([]byte, error) {
   105  	v, err := yamlcomment.Marshal(p)
   106  	if err != nil {
   107  		return nil, errs.Wrap(err, "Could not marshal publish request")
   108  	}
   109  
   110  	if includeExample {
   111  		if len(p.Authors) == 0 {
   112  			exampleAuthorYaml, err := yamlcomment.Marshal(exampleAuthor)
   113  			if err != nil {
   114  				return nil, errs.Wrap(err, "Could not marshal example author")
   115  			}
   116  			exampleAuthorYaml = append([]byte("# "), bytes.ReplaceAll(exampleAuthorYaml, []byte("\n"), []byte("\n# "))...)
   117  			exampleAuthorYaml = append([]byte("\n## Optional -- Example Author:\n"), exampleAuthorYaml...)
   118  			v = append(v, exampleAuthorYaml...)
   119  		}
   120  
   121  		if len(p.Dependencies) == 0 {
   122  			exampleDepYaml, err := yamlcomment.Marshal(exampleDep)
   123  			if err != nil {
   124  				return nil, errs.Wrap(err, "Could not marshal example deps")
   125  			}
   126  			exampleDepYaml = append([]byte("# "), bytes.ReplaceAll(exampleDepYaml, []byte("\n"), []byte("\n# "))...)
   127  			exampleDepYaml = append([]byte("\n## Optional -- Example Dependencies:\n"), exampleDepYaml...)
   128  			v = append(v, exampleDepYaml...)
   129  		}
   130  	}
   131  
   132  	return v, nil
   133  }
   134  
   135  func (p *PublishVariables) UnmarshalYaml(b []byte) error {
   136  	return yaml.Unmarshal(b, p)
   137  }
   138  
   139  var exampleAuthor = ExampleAuthorVariables{[]PublishVariableAuthor{{
   140  	Name:     "John Doe",
   141  	Email:    "johndoe@domain.tld",
   142  	Websites: []string{"https://example.com"},
   143  }}}
   144  
   145  var exampleDep = ExampleDepVariables{[]PublishVariableDep{{
   146  	Dependency{
   147  		Name:                "example-linux-specific-ingredient",
   148  		Namespace:           "shared",
   149  		VersionRequirements: ">= 1.0.0",
   150  	},
   151  	[]Dependency{
   152  		{
   153  			Name:                "linux",
   154  			Namespace:           "kernel",
   155  			VersionRequirements: ">= 0",
   156  		},
   157  	},
   158  }}}
   159  
   160  type PublishInput struct {
   161  	file      *os.File
   162  	Variables PublishVariables
   163  }
   164  
   165  func (p *PublishInput) Close() error {
   166  	return p.file.Close()
   167  }
   168  
   169  func (p *PublishInput) Files() []gqlclient.File {
   170  	if p.file == nil {
   171  		return []gqlclient.File{}
   172  	}
   173  	return []gqlclient.File{
   174  		{
   175  			Field: "variables.input.file", // this needs to map to the graphql input, eg. variables.input.file
   176  			Name:  p.Variables.Name,
   177  			R:     p.file,
   178  		},
   179  	}
   180  }
   181  
   182  func (p *PublishInput) Query() string {
   183  	return `
   184  		mutation ($input: PublishInput!) {
   185  			publish(input: $input) {
   186  				... on CreatedIngredientVersionRevision {
   187  					ingredientID
   188  					ingredientVersionID
   189  					revision
   190  				}
   191  				... on Error{
   192  					__typename
   193  					error: message
   194  				}
   195  			}
   196  		}
   197  `
   198  }
   199  
   200  func (p *PublishInput) Vars() (map[string]interface{}, error) {
   201  	// Path is only used when sending data to graphql, so rather than updating it multiple times as source vars
   202  	// are changed we just set it here once prior to its use.
   203  	p.Variables.Path = p.Variables.Namespace + "/" + p.Variables.Name
   204  
   205  	// Convert our json data to a map
   206  	vars, err := json.Marshal(p.Variables)
   207  	if err != nil {
   208  		return nil, errs.Wrap(err, "Could not marshal publish input vars")
   209  	}
   210  	varMap := make(map[string]interface{})
   211  	if err := json.Unmarshal(vars, &varMap); err != nil {
   212  		return nil, errs.Wrap(err, "Could not unmarshal publish input vars")
   213  	}
   214  
   215  	return map[string]interface{}{
   216  		"input": varMap,
   217  	}, nil
   218  }