github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/charmhub/find.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package charmhub 5 6 import ( 7 "context" 8 "net/http" 9 "strings" 10 11 "github.com/juju/errors" 12 13 "github.com/juju/juju/charmhub/path" 14 "github.com/juju/juju/charmhub/transport" 15 ) 16 17 // FindOption to be passed to Find to customize the resulting request. 18 type FindOption func(*findOptions) 19 20 type findOptions struct { 21 category *string 22 channel *string 23 charmType *string 24 platforms *string 25 publisher *string 26 relationRequires *string 27 relationProvides *string 28 } 29 30 // WithFindCategory sets the category on the option. 31 func WithFindCategory(category string) FindOption { 32 return func(findOptions *findOptions) { 33 findOptions.category = &category 34 } 35 } 36 37 // WithFindChannel sets the channel on the option. 38 func WithFindChannel(channel string) FindOption { 39 return func(findOptions *findOptions) { 40 findOptions.channel = &channel 41 } 42 } 43 44 // WithFindType sets the charmType on the option. 45 func WithFindType(charmType string) FindOption { 46 return func(findOptions *findOptions) { 47 findOptions.charmType = &charmType 48 } 49 } 50 51 // WithFindPlatforms sets the charmPlatforms on the option. 52 func WithFindPlatforms(platforms string) FindOption { 53 return func(findOptions *findOptions) { 54 findOptions.platforms = &platforms 55 } 56 } 57 58 // WithFindPublisher sets the publisher on the option. 59 func WithFindPublisher(publisher string) FindOption { 60 return func(findOptions *findOptions) { 61 findOptions.publisher = &publisher 62 } 63 } 64 65 // WithFindRelationRequires sets the relationRequires on the option. 66 func WithFindRelationRequires(relationRequires string) FindOption { 67 return func(findOptions *findOptions) { 68 findOptions.relationRequires = &relationRequires 69 } 70 } 71 72 // WithFindRelationProvides sets the relationProvides on the option. 73 func WithFindRelationProvides(relationProvides string) FindOption { 74 return func(findOptions *findOptions) { 75 findOptions.relationProvides = &relationProvides 76 } 77 } 78 79 // Create a findOptions instance with default values. 80 func newFindOptions() *findOptions { 81 return &findOptions{} 82 } 83 84 // findClient defines a client for querying information about a given charm or 85 // bundle for a given CharmHub store. 86 type findClient struct { 87 path path.Path 88 client RESTClient 89 logger Logger 90 } 91 92 // newFindClient creates a findClient for querying charm or bundle information. 93 func newFindClient(path path.Path, client RESTClient, logger Logger) *findClient { 94 return &findClient{ 95 path: path, 96 client: client, 97 logger: logger, 98 } 99 } 100 101 // Find searches Charm Hub and provides results matching a string. 102 func (c *findClient) Find(ctx context.Context, query string, options ...FindOption) ([]transport.FindResponse, error) { 103 opts := newFindOptions() 104 for _, option := range options { 105 option(opts) 106 } 107 108 c.logger.Tracef("Find(%s)", query) 109 path, err := c.path.Query("q", query) 110 if err != nil { 111 return nil, errors.Trace(err) 112 } 113 114 path, err = path.Query("fields", defaultFindFilter()) 115 if err != nil { 116 return nil, errors.Trace(err) 117 } 118 119 if err := walkFindOptions(opts, func(name, value string) error { 120 path, err = path.Query(name, value) 121 return errors.Trace(err) 122 }); err != nil { 123 return nil, errors.Trace(err) 124 } 125 126 var resp transport.FindResponses 127 restResp, err := c.client.Get(ctx, path, &resp) 128 if err != nil { 129 return nil, errors.Trace(err) 130 } 131 if restResp.StatusCode == http.StatusNotFound { 132 return nil, errors.NotFoundf(query) 133 } 134 if err := handleBasicAPIErrors(resp.ErrorList, c.logger); err != nil { 135 return nil, errors.Trace(err) 136 } 137 138 return resp.Results, nil 139 } 140 141 func walkFindOptions(opts *findOptions, fn func(string, string) error) error { 142 // We could use reflect here, but it might be easier to just list out what 143 // we want to walk over. 144 // See: https://gist.github.com/SimonRichardson/7c9243d71551cad4af7661128add93b5 145 if opts.category != nil { 146 if err := fn("category", *opts.category); err != nil { 147 return errors.Trace(err) 148 } 149 } 150 if opts.channel != nil { 151 if err := fn("channel", *opts.channel); err != nil { 152 return errors.Trace(err) 153 } 154 } 155 if opts.charmType != nil { 156 if err := fn("type", *opts.charmType); err != nil { 157 return errors.Trace(err) 158 } 159 } 160 if opts.platforms != nil { 161 if err := fn("platforms", *opts.platforms); err != nil { 162 return errors.Trace(err) 163 } 164 } 165 if opts.publisher != nil { 166 if err := fn("publisher", *opts.publisher); err != nil { 167 return errors.Trace(err) 168 } 169 } 170 if opts.relationRequires != nil { 171 if err := fn("relation-requires", *opts.relationRequires); err != nil { 172 return errors.Trace(err) 173 } 174 } 175 if opts.relationProvides != nil { 176 if err := fn("relation-provides", *opts.relationProvides); err != nil { 177 return errors.Trace(err) 178 } 179 } 180 return nil 181 } 182 183 // defaultFindFilter returns a filter string to retrieve all data 184 // necessary to fill the transport.FindResponse. Without it, we'd 185 // receive the Name, ID and Type. 186 func defaultFindFilter() string { 187 filter := defaultFindResultFilter 188 filter = append(filter, appendFilterList("default-release", defaultRevisionFilter)...) 189 return strings.Join(filter, ",") 190 } 191 192 var defaultFindResultFilter = []string{ 193 "result.publisher.display-name", 194 "result.summary", 195 "result.store-url", 196 } 197 198 var defaultRevisionFilter = []string{ 199 "revision.bases.architecture", 200 "revision.bases.name", 201 "revision.bases.channel", 202 "revision.version", 203 }