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 }